feat: move routes to use rate-limit macros
This commit is contained in:
parent
8ae905a2c1
commit
6499c0a9ab
169
asteroid.lisp
169
asteroid.lisp
|
|
@ -1128,16 +1128,16 @@
|
||||||
|#
|
|#
|
||||||
|
|
||||||
;; Auth status API endpoint
|
;; Auth status API endpoint
|
||||||
(define-api asteroid/auth-status () ()
|
(define-api-with-limit asteroid/auth-status () ()
|
||||||
"Check if user is logged in and their role"
|
"Check if user is logged in and their role"
|
||||||
(with-error-handling
|
(with-error-handling
|
||||||
(let* ((user-id (session:field "user-id"))
|
(let* ((user-id (session:field "user-id"))
|
||||||
(user (when user-id (find-user-by-id user-id))))
|
(user (when user-id (find-user-by-id user-id))))
|
||||||
(api-output `(("loggedIn" . ,(if user t nil))
|
(api-output `(("loggedIn" . ,(if user t nil))
|
||||||
("isAdmin" . ,(if (and user (user-has-role-p user :admin)) t nil))
|
("isAdmin" . ,(if (and user (user-has-role-p user :admin)) t nil))
|
||||||
("username" . ,(if user
|
("username" . ,(if user
|
||||||
(dm:field user "username")
|
(dm:field user "username")
|
||||||
nil)))))))
|
nil)))))))
|
||||||
|
|
||||||
;; User profile API endpoints
|
;; User profile API endpoints
|
||||||
(define-api asteroid/user/profile () ()
|
(define-api asteroid/user/profile () ()
|
||||||
|
|
@ -1199,66 +1199,66 @@
|
||||||
artists)))))))
|
artists)))))))
|
||||||
|
|
||||||
;; Register page (GET)
|
;; Register page (GET)
|
||||||
(define-page register #@"/register" ()
|
(define-page-with-limit register #@"/register" ()
|
||||||
"User registration page"
|
"User registration page"
|
||||||
(let ((username (radiance:post-var "username"))
|
(let ((username (radiance:post-var "username"))
|
||||||
(email (radiance:post-var "email"))
|
(email (radiance:post-var "email"))
|
||||||
(password (radiance:post-var "password"))
|
(password (radiance:post-var "password"))
|
||||||
(confirm-password (radiance:post-var "confirm-password")))
|
(confirm-password (radiance:post-var "confirm-password")))
|
||||||
(if (and username password)
|
(if (and username password)
|
||||||
;; Handle registration form submission
|
;; Handle registration form submission
|
||||||
(cond
|
(cond
|
||||||
;; Validate passwords match
|
;; Validate passwords match
|
||||||
((not (string= password confirm-password))
|
((not (string= password confirm-password))
|
||||||
(format t "Failed to register new user '~a': passwords do not match.~%" username)
|
(format t "Failed to register new user '~a': passwords do not match.~%" username)
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(load-template "register")
|
(load-template "register")
|
||||||
:title "Asteroid Radio - Register"
|
:title "Asteroid Radio - Register"
|
||||||
:display-error "display: block;"
|
:display-error "display: block;"
|
||||||
:display-success "display: none;"
|
:display-success "display: none;"
|
||||||
:error-message "Passwords do not match"
|
:error-message "Passwords do not match"
|
||||||
:success-message ""))
|
:success-message ""))
|
||||||
|
|
||||||
;; Check if username already exists
|
|
||||||
((find-user-by-username username)
|
|
||||||
(format t "Failed to register new user '~a': Username already exists.~%" username)
|
|
||||||
(clip:process-to-string
|
|
||||||
(load-template "register")
|
|
||||||
:title "Asteroid Radio - Register"
|
|
||||||
:display-error "display: block;"
|
|
||||||
:display-success "display: none;"
|
|
||||||
:error-message "Username already exists"
|
|
||||||
:success-message ""))
|
|
||||||
|
|
||||||
;; Create the user
|
|
||||||
(t
|
|
||||||
(if (create-user username email password :role :listener :active t)
|
|
||||||
(progn
|
|
||||||
;; Auto-login after successful registration
|
|
||||||
(let ((user (find-user-by-username username)))
|
|
||||||
(when user
|
|
||||||
(let ((user-id (dm:id user)))
|
|
||||||
(setf (session:field "user-id") user-id))))
|
|
||||||
;; Redirect new users to their profile page
|
|
||||||
(radiance:redirect "/asteroid/profile"))
|
|
||||||
(clip:process-to-string
|
|
||||||
(load-template "register")
|
|
||||||
:title "Asteroid Radio - Register"
|
|
||||||
:display-error "display: block;"
|
|
||||||
:display-success "display: none;"
|
|
||||||
:error-message "Registration failed. Please try again."
|
|
||||||
:success-message ""))))
|
|
||||||
;; Show registration form (no POST data)
|
|
||||||
(clip:process-to-string
|
|
||||||
(load-template "register")
|
|
||||||
:title "Asteroid Radio - Register"
|
|
||||||
:display-error "display: none;"
|
|
||||||
:display-success "display: none;"
|
|
||||||
:error-message ""
|
|
||||||
:success-message ""))))
|
|
||||||
|
|
||||||
(define-page player #@"/player" ()
|
;; Check if username already exists
|
||||||
(clip:process-to-string
|
((find-user-by-username username)
|
||||||
|
(format t "Failed to register new user '~a': Username already exists.~%" username)
|
||||||
|
(clip:process-to-string
|
||||||
|
(load-template "register")
|
||||||
|
:title "Asteroid Radio - Register"
|
||||||
|
:display-error "display: block;"
|
||||||
|
:display-success "display: none;"
|
||||||
|
:error-message "Username already exists"
|
||||||
|
:success-message ""))
|
||||||
|
|
||||||
|
;; Create the user
|
||||||
|
(t
|
||||||
|
(if (create-user username email password :role :listener :active t)
|
||||||
|
(progn
|
||||||
|
;; Auto-login after successful registration
|
||||||
|
(let ((user (find-user-by-username username)))
|
||||||
|
(when user
|
||||||
|
(let ((user-id (dm:id user)))
|
||||||
|
(setf (session:field "user-id") user-id))))
|
||||||
|
;; Redirect new users to their profile page
|
||||||
|
(radiance:redirect "/asteroid/profile"))
|
||||||
|
(clip:process-to-string
|
||||||
|
(load-template "register")
|
||||||
|
:title "Asteroid Radio - Register"
|
||||||
|
:display-error "display: block;"
|
||||||
|
:display-success "display: none;"
|
||||||
|
:error-message "Registration failed. Please try again."
|
||||||
|
:success-message ""))))
|
||||||
|
;; Show registration form (no POST data)
|
||||||
|
(clip:process-to-string
|
||||||
|
(load-template "register")
|
||||||
|
:title "Asteroid Radio - Register"
|
||||||
|
:display-error "display: none;"
|
||||||
|
:display-success "display: none;"
|
||||||
|
:error-message ""
|
||||||
|
:success-message ""))))
|
||||||
|
|
||||||
|
(define-page-with-limit player #@"/player" (:limit-group "public")
|
||||||
|
(clip:process-to-string
|
||||||
(load-template "player")
|
(load-template "player")
|
||||||
:title "Asteroid Radio - Web Player"
|
:title "Asteroid Radio - Web Player"
|
||||||
:stream-base-url *stream-base-url*
|
:stream-base-url *stream-base-url*
|
||||||
|
|
@ -1270,18 +1270,18 @@
|
||||||
:player-status "Stopped"))
|
:player-status "Stopped"))
|
||||||
|
|
||||||
;; Player content frame (for frameset mode)
|
;; Player content frame (for frameset mode)
|
||||||
(define-page player-content #@"/player-content" ()
|
(define-page-with-limit player-content #@"/player-content" (:limit-group "public")
|
||||||
"Player page content (displayed in content frame)"
|
"Player page content (displayed in content frame)"
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(load-template "player-content")
|
(load-template "player-content")
|
||||||
:title "Asteroid Radio - Web Player"
|
:title "Asteroid Radio - Web Player"
|
||||||
:stream-base-url *stream-base-url*
|
:stream-base-url *stream-base-url*
|
||||||
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
:default-stream-url (format nil "~a/asteroid.aac" *stream-base-url*)
|
||||||
:default-stream-encoding "audio/aac"))
|
:default-stream-encoding "audio/aac"))
|
||||||
|
|
||||||
(define-page popout-player #@"/popout-player" ()
|
(define-page-with-limit popout-player #@"/popout-player" (:limit-group "public")
|
||||||
"Pop-out player window"
|
"Pop-out player window"
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(load-template "popout-player")
|
(load-template "popout-player")
|
||||||
:stream-base-url *stream-base-url*
|
:stream-base-url *stream-base-url*
|
||||||
:curated-channel-name (get-curated-channel-name)
|
:curated-channel-name (get-curated-channel-name)
|
||||||
|
|
@ -1289,27 +1289,27 @@
|
||||||
:default-stream-encoding "audio/aac"))
|
:default-stream-encoding "audio/aac"))
|
||||||
|
|
||||||
;; About page (non-frameset mode)
|
;; About page (non-frameset mode)
|
||||||
(define-page about-page #@"/about" ()
|
(define-page-with-limit about-page #@"/about" (:limit-group "public")
|
||||||
"About Asteroid Radio"
|
"About Asteroid Radio"
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(load-template "about")
|
(load-template "about")
|
||||||
:title "About - Asteroid Radio"))
|
:title "About - Asteroid Radio"))
|
||||||
|
|
||||||
;; About content (for frameset mode)
|
;; About content (for frameset mode)
|
||||||
(define-page about-content #@"/about-content" ()
|
(define-page-with-limit about-content #@"/about-content" (:limit-group "public")
|
||||||
"About page content (displayed in content frame)"
|
"About page content (displayed in content frame)"
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(load-template "about-content")
|
(load-template "about-content")
|
||||||
:title "About - Asteroid Radio"))
|
:title "About - Asteroid Radio"))
|
||||||
|
|
||||||
;; Status content (for frameset mode)
|
;; Status content (for frameset mode)
|
||||||
(define-page status-content #@"/status-content" ()
|
(define-page-with-limit status-content #@"/status-content" (:limit-group "public")
|
||||||
"Status page content (displayed in content frame)"
|
"Status page content (displayed in content frame)"
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(load-template "status-content")
|
(load-template "status-content")
|
||||||
:title "Status - Asteroid Radio"))
|
:title "Status - Asteroid Radio"))
|
||||||
|
|
||||||
(define-api asteroid/status () ()
|
(define-api-with-limit asteroid/status () ()
|
||||||
"Get server status"
|
"Get server status"
|
||||||
(api-output `(("status" . "running")
|
(api-output `(("status" . "running")
|
||||||
("server" . "asteroid-radio")
|
("server" . "asteroid-radio")
|
||||||
|
|
@ -1323,23 +1323,23 @@
|
||||||
("stream-status" . "live"))))
|
("stream-status" . "live"))))
|
||||||
|
|
||||||
;; Live stream status from Icecast
|
;; Live stream status from Icecast
|
||||||
(define-api asteroid/icecast-status () ()
|
(define-api-with-limit asteroid/icecast-status () ()
|
||||||
"Get live status from Icecast server"
|
"Get live status from Icecast server"
|
||||||
(with-error-handling
|
(with-error-handling
|
||||||
(let* ((icecast-url (format nil "~a/admin/stats.xml" *stream-base-url*))
|
(let* ((icecast-url (format nil "~a/admin/stats.xml" *stream-base-url*))
|
||||||
(response (drakma:http-request icecast-url
|
(response (drakma:http-request icecast-url
|
||||||
:want-stream nil
|
:want-stream nil
|
||||||
:basic-authorization '("admin" "asteroid_admin_2024"))))
|
:basic-authorization '("admin" "asteroid_admin_2024"))))
|
||||||
(if response
|
(if response
|
||||||
(let ((xml-string (if (stringp response)
|
(let ((xml-string (if (stringp response)
|
||||||
response
|
response
|
||||||
(babel:octets-to-string response :encoding :utf-8))))
|
(babel:octets-to-string response :encoding :utf-8))))
|
||||||
;; Simple XML parsing to extract source information
|
;; Simple XML parsing to extract source information
|
||||||
;; Look for <source mount="/asteroid.mp3"> sections and extract title, listeners, etc.
|
;; Look for <source mount="/asteroid.mp3"> sections and extract title, listeners, etc.
|
||||||
(multiple-value-bind (match-start match-end)
|
(multiple-value-bind (match-start match-end)
|
||||||
(cl-ppcre:scan "<source mount=\"/asteroid\\.mp3\">" xml-string)
|
(cl-ppcre:scan "<source mount=\"/asteroid\\.mp3\">" xml-string)
|
||||||
(if match-start
|
(if match-start
|
||||||
(let* ((source-section (subseq xml-string match-start
|
(let* ((source-section (subseq xml-string match-start
|
||||||
(or (cl-ppcre:scan "</source>" xml-string :start match-start)
|
(or (cl-ppcre:scan "</source>" xml-string :start match-start)
|
||||||
(length xml-string))))
|
(length xml-string))))
|
||||||
(titlep (cl-ppcre:all-matches "<title>" source-section))
|
(titlep (cl-ppcre:all-matches "<title>" source-section))
|
||||||
|
|
@ -1360,12 +1360,13 @@
|
||||||
|
|
||||||
;;; Listener Statistics API Endpoints
|
;;; Listener Statistics API Endpoints
|
||||||
|
|
||||||
(define-api asteroid/stats/current () ()
|
(define-api-with-limit asteroid/stats/current () ()
|
||||||
"Get current listener count from recent snapshots"
|
"Get current listener count from recent snapshots"
|
||||||
(let ((listeners (get-current-listeners)))
|
(with-error-handling
|
||||||
(api-output `(("status" . "success")
|
(let ((listeners (get-current-listeners)))
|
||||||
("listeners" . ,listeners)
|
(api-output `(("status" . "success")
|
||||||
("timestamp" . ,(get-universal-time))))))
|
("listeners" . ,listeners)
|
||||||
|
("timestamp" . ,(get-universal-time)))))))
|
||||||
|
|
||||||
(define-api asteroid/stats/daily (&optional (days "30")) ()
|
(define-api asteroid/stats/daily (&optional (days "30")) ()
|
||||||
"Get daily listener statistics (admin only)"
|
"Get daily listener statistics (admin only)"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
(in-package :asteroid)
|
(in-package :asteroid)
|
||||||
|
|
||||||
;; Login page (GET)
|
;; Login page (GET)
|
||||||
(define-page login #@"/login" ()
|
(define-page-with-limit login #@"/login" ()
|
||||||
"User login page"
|
"User login page"
|
||||||
(let ((username (radiance:post-var "username"))
|
(let ((username (radiance:post-var "username"))
|
||||||
(password (radiance:post-var "password")))
|
(password (radiance:post-var "password")))
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
(:listeners . ,total-listeners)
|
(:listeners . ,total-listeners)
|
||||||
(:track-id . ,(find-track-by-title title))))))))
|
(:track-id . ,(find-track-by-title title))))))))
|
||||||
|
|
||||||
(define-api asteroid/partial/now-playing (&optional mount) ()
|
(define-api-with-limit asteroid/partial/now-playing (&optional mount) ()
|
||||||
"Get Partial HTML with live status from Icecast server.
|
"Get Partial HTML with live status from Icecast server.
|
||||||
Optional MOUNT parameter specifies which stream to get metadata from.
|
Optional MOUNT parameter specifies which stream to get metadata from.
|
||||||
Always polls both streams to keep recently played lists updated."
|
Always polls both streams to keep recently played lists updated."
|
||||||
|
|
@ -105,7 +105,7 @@
|
||||||
:connection-error t
|
:connection-error t
|
||||||
:stats nil))))))
|
:stats nil))))))
|
||||||
|
|
||||||
(define-api asteroid/partial/now-playing-inline (&optional mount) ()
|
(define-api-with-limit asteroid/partial/now-playing-inline (&optional mount) ()
|
||||||
"Get inline text with now playing info (for admin dashboard and widgets).
|
"Get inline text with now playing info (for admin dashboard and widgets).
|
||||||
Optional MOUNT parameter specifies which stream to get metadata from."
|
Optional MOUNT parameter specifies which stream to get metadata from."
|
||||||
(with-error-handling
|
(with-error-handling
|
||||||
|
|
@ -119,7 +119,7 @@
|
||||||
(setf (header "Content-Type") "text/plain")
|
(setf (header "Content-Type") "text/plain")
|
||||||
"Stream Offline")))))
|
"Stream Offline")))))
|
||||||
|
|
||||||
(define-api asteroid/partial/now-playing-json (&optional mount) ()
|
(define-api-with-limit asteroid/partial/now-playing-json (&optional mount) (:limit 2 :timeout 1)
|
||||||
"Get JSON with now playing info including track ID for favorites.
|
"Get JSON with now playing info including track ID for favorites.
|
||||||
Optional MOUNT parameter specifies which stream to get metadata from."
|
Optional MOUNT parameter specifies which stream to get metadata from."
|
||||||
(with-error-handling
|
(with-error-handling
|
||||||
|
|
@ -137,7 +137,7 @@
|
||||||
("title" . "Stream Offline")
|
("title" . "Stream Offline")
|
||||||
("track_id" . nil)))))))
|
("track_id" . nil)))))))
|
||||||
|
|
||||||
(define-api asteroid/channel-name () ()
|
(define-api-with-limit asteroid/channel-name () (:limit 2 :timeout 1)
|
||||||
"Get the current curated channel name for live updates.
|
"Get the current curated channel name for live updates.
|
||||||
Returns JSON with the channel name from the current playlist's PHASE header."
|
Returns JSON with the channel name from the current playlist's PHASE header."
|
||||||
(with-error-handling
|
(with-error-handling
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue