Compare commits

..

9 Commits

Author SHA1 Message Date
Luis Pereira d894964c20 fix: increase now-playing rate limit to 10 per second 2026-01-23 21:17:44 -05:00
Luis Pereira 6b56a17b4a fix: restore AUTHSTATE object when there is no login 2026-01-23 21:17:44 -05:00
Luis Pereira 0def454077 feat: use navbar partial on all page templates 2026-01-21 17:32:41 -05:00
Luis Pereira 7c6eaa1fe0 feat: adds templating system docs 2026-01-21 17:32:41 -05:00
Luis Pereira 2992822010 feat: replace global auth state variable with template injected object 2026-01-21 17:32:41 -05:00
Luis Pereira 6ab7489f9b feat: allows for navbar menu exclution with exclude list 2026-01-21 17:32:41 -05:00
Luis Pereira cffb3cf384 feat: reusable navbar with auth check in render 2026-01-21 17:32:41 -05:00
Luis Pereira 732ff0858e fix: hide analyzer when in frame player mode 2026-01-21 17:32:41 -05:00
glenneth 55eafa943f Fix rate limiter corruption: cleanup negative amounts on startup
The r-simple-rate library has a bug where rate limit counters can go
negative and never reset. This happens because the reset condition
only triggers when amount >= 0, so negative amounts are permanently
stuck.

This fix adds:
- cleanup-corrupted-rate-limits function to delete corrupted entries
- db:connected trigger to run cleanup automatically on startup

This prevents the 429 error loops that occurred when counters became
corrupted with large negative values.
2026-01-18 12:58:35 -05:00
18 changed files with 332 additions and 154 deletions

View File

@ -4,7 +4,8 @@
(:use :cl)
(:export :internal-disable-debugger)
(:export :internal-quit
:pht))
:pht
:member-string))
(in-package :asteroid.app-utils)
@ -19,6 +20,11 @@
(internal-quit)))
(setf *debugger-hook* #'internal-exit)))
(defun member-string (item seq)
"Checkes if a string 'item' is a member of a list. Returns t or nil for the finding result."
(when (member item seq :test #'string-equal)
t))
(defun internal-quit (&optional code)
"Taken from the cliki"
;; This group from "clocc-port/ext.lisp"

View File

@ -1009,6 +1009,7 @@
(error () 0))))
(clip:process-to-string
(load-template "admin")
:navbar-exclude '("admin")
:title "🎵 ASTEROID RADIO - Admin Dashboard"
:server-status "🟢 Running"
:database-status (handler-case
@ -1027,6 +1028,7 @@
(require-authentication)
(clip:process-to-string
(load-template "users")
:navbar-exclude '("profile" "users")
:title "ASTEROID RADIO - User Management"))
;; User Profile page (requires authentication)
@ -1035,6 +1037,7 @@
(require-authentication)
(clip:process-to-string
(load-template "profile")
:navbar-exclude '("about" "status" "profile")
:title "🎧 admin - Profile | Asteroid Radio"
:username "admin"
:user-role "admin"

160
docs/TEMPLATING_SYSTEM.org Normal file
View File

@ -0,0 +1,160 @@
#+TITLE: Templating System
#+AUTHOR: Asteroid Radio Development Team
#+DATE: 2026-01-17
* Introduction
The radiance ecosystem includes an HTML templating system that is built on top of [[https://quickdocs.org/clip][Clip]] and [[https://shinmera.github.io/lquery/][lquery]] for advanced costumization.
While =clip= enables conditional logic on rendering parts in an HTML document, re-using the same tag mechanics, =lquery= enables dynamically setting of element properties with some lisp integrations.
Dominating these tools enables some smarter approaches on page content building with conditional rendering and code reuse.
* Clip mechanics
** Conditionals
Clip uses HTML tags for its logic and has 3 simple conditional directives: =if=, =when= and =unless=, that work exactly as their lisp counterparts, having the =test= attribute as the validator for truthness.
The =if= check is accomplished by the =c:if= tag, and requires a =c:then= tag for the body of its branch. It also has =c:else= and =c:elseif= to enable multiple branching:
#+begin_src html
<c:if test='framesetp'>
<c:then>
<!-- HTML test true branch -->
</c:then>
<c:else>
<!-- HTML test false branch -->
</c:else>
</c:if>
#+end_src
The above example tests for the truthness of the variable =framesetp= and executes the associate branch based on that. The =test= argument can, naturally, receive any kind of lisp syntax.
The =c:when= and =c:unless= directives, like their lisp counterparts, only have a branch that is executed when the check of =test= argument value is true or false accordingly.
*Note:* Clip also has =c:case= and =c:cond= conditionals that should work like they lisp counterparts but I didn't need them until now so I'll not try to explain them.
** Data usage
Using a variable content in the template engine is accomplished by the =lquery= tag attribute, which will be explained later. But as this is a tag attribute, it requires an HTML tag to be attached. To enable the possibility of inserting something anywhere on the HTML document, Clip supplies the =c:splice= tag, a virtual HTML node that is removed after rendering, leaving only the content inserted by =lquery=.
The follow example inserts a textual value of the =content= variable directly into the HTML page:
#+begin_src html
<c:splice lquery='(text content)></c:splice>
#+end_src
However, a common use case is inserting external HTML rendered content in a different HTML document, for example, to reuse a navbar HTML partial in every page:
#+begin_src html
<c:splice lquery='(html navbar)></c:splice>
#+end_src
Those two usages are so frequently that Clip supplies shorhand aliases for both as the =c:s= and =c:h= tags:
#+begin_src html
<!-- Insert a textual value -->
<c:s>content</c:s>
<!-- Insert an HTML partial to render -->
<c:h>navbar</c:h>
#+end_src
A great usage of this feature is our new navbar reusable HTML partial, that can be loaded from a speficic lisp function on any page:
#+begin_src html
<c:h>(asteroid::load-template "partial/navbar")</c:h>
#+end_src
*Note:* As far as I understand, the templating engine works on a different package, so refering to the =asteroid= package is always required inside a template to use defined functions.
** Clipboard environments
Clip uses the concept of environment as the data that is available to be used in the templating system.
The =c:let= directive lets define a new clipboard environment directly on the template. This will supersede the default clipboard, so any useful data needs to also be added to the clipboard for easy access:
#+begin_src html
<c:let new-var='"hello"' forward='var'>
<!-- HTML content -->
</c:let>
#+end_src
The above example defines a local =new-var= with the value ="hello"= and a local =forward= with the parent =var= variable content.
Alternatively, if forwarding a variable to the new clipboard environment is not desired, the special =(** :var)= syntax can be used to access the parent clipboard.
The =c:using= directive creates a new clipboard environment with the contents of an existing variable on the parent clipboard.
#+begin_src html
<c:using value='obj'>
<!-- HTML content using the 'obj.attribute1' value-->
<c:splice>attribute1</c:splice>
</c:let>
#+end_src
The example creates a new clipboard environment inside the =c:using= attribute that has direct access to all properties of some structured variable =obj=. This tag transparently destructures any kind of composed data structure like =plists=, =alists= and =classes=.
Like the simplicity of structural access of the =c:using= tag, the =clip= function enables the same easy of access in any template context. For example, the same splice of the previous example can be obtained with:
#+begin_src html
<c:s>(clip obj :attribute1)</c:s>
#+end_src
This approach is, clearly, more useful when a single value is wanted, while the previous when several values on the structured variable will be used.
** Iterators
Clip also has the =c:iterate= directive that iterates a sequence, and its body is rendered having the clipboard environment matching each element of that iteration. As I am yet to use this directive, I will leave its documentation to a later moment.
* lquery mechanics
The [[https://shinmera.github.io/lquery/][lquery]] library enables changing any property of an HTML node, being its =text= or =html= content, =css= styles, element attributes (=attr=) and assigned css classes.
Its usage is very simple: an =lquery= attribute is added to an HTML document, and its value is a s-expression with an accepted funtion and the value for it to work on.
The following topics present some basic accepted =lquery= functions in use on our templates.
** HTML node content
There are two basic =lquery= functions that change the content of the assigned HTML node:
- =html= renders HTML elements as child
- =text= sets a text content to the element
The =lquery= s-expression accepts variable or lisp calls for any of the referred functions. For example, the following example sets the content of the =script= tag to the result of the =(asteroid::get-auth-state-js-var)= function:
#+begin_src html
<script lquery='(text (asteroid::get-auth-state-js-var))'></script>
#+end_src
*Note:* the template render process works on a different package, so call to application defined functions needs to include the package name.
** HTML node styling
=lquery= also grants easy styling customization of an HTML node. HTML styling can be accomplished in two ways:
- =css=, which sets the HTML =style= attribute of the element. It's called as a plist with all the properties to set.
- css classes that can be added (=add-class=) or removed (=remove-class=)
The following example adds the =display= and =marging= style properties to the document when the variable =framesetp= is true (=margin= is overwriten in this case):
#+begin_src html
<div style="margin: 15px 0;" lquery='(css :display (when framesetp "none") :margin (when framesetp "0"))'>
#+end_src
** HTML attributes
The =attr= function of the =lquery= s-expression allows customization of any valid HTML node attribute. The following example sets two attributes of an anchor tag:
- =href=, which uses the =eval= function to evaluate some lisp call
- =target= which only has a value when the variable =framesetp= is true
#+begin_src html
<a lquery='(attr :href (eval (format nil "/asteroid/~a" status-href)) :target (when framesetp "_self"))'>
#+end_src

View File

@ -91,7 +91,7 @@
(:listeners . ,total-listeners)
(:track-id . ,(find-track-by-title title))))))))
(define-api-with-limit asteroid/partial/now-playing (&optional mount) (:limit 180 :timeout 60)
(define-api-with-limit asteroid/partial/now-playing (&optional mount) (:limit 10 :timeout 1)
"Get Partial HTML with live status from Icecast server.
Optional MOUNT parameter specifies which stream to get metadata from.
Always polls both streams to keep recently played lists updated."
@ -121,7 +121,7 @@
:connection-error t
:stats nil))))))
(define-api-with-limit asteroid/partial/now-playing-inline (&optional mount) (:limit 180 :timeout 60)
(define-api-with-limit asteroid/partial/now-playing-inline (&optional mount) (:limit 10 :timeout 1)
"Get inline text with now playing info (for admin dashboard and widgets).
Optional MOUNT parameter specifies which stream to get metadata from."
(with-error-handling
@ -135,7 +135,7 @@
(setf (header "Content-Type") "text/plain")
"Stream Offline")))))
(define-api-with-limit asteroid/partial/now-playing-json (&optional mount) (:limit 180 :timeout 60)
(define-api-with-limit asteroid/partial/now-playing-json (&optional mount) (:limit 10 :timeout 1)
"Get JSON with now playing info including track ID for favorites.
Optional MOUNT parameter specifies which stream to get metadata from."
;; Register web listener for geo stats (keeps listener active during playback)

View File

@ -2,6 +2,23 @@
(in-package :asteroid)
(defun cleanup-corrupted-rate-limits ()
"Clean up corrupted rate limit entries with negative amounts.
The r-simple-rate library has a bug where the reset condition only triggers
when amount >= 0, so negative amounts never reset. This function deletes
any corrupted entries so they can be recreated fresh."
(handler-case
(let ((deleted (db:remove 'simple-rate::tracking
(db:query (:< 'amount 0)))))
(when (and deleted (> deleted 0))
(l:info :rate-limiter "Cleaned up ~a corrupted rate limit entries" deleted)))
(error (e)
(l:warn :rate-limiter "Failed to cleanup rate limits: ~a" e))))
(define-trigger db:connected ()
"Clean up any corrupted rate limit entries on startup"
(cleanup-corrupted-rate-limits))
(defun render-rate-limit-error-page()
(clip:process-to-string
(load-template "error")

View File

@ -8,7 +8,7 @@
<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">
<script src="/asteroid/static/js/auth-ui.js"></script>
<script lquery='(text (asteroid::get-auth-state-js-var))'></script>
</head>
<body>
<div class="container">
@ -19,33 +19,9 @@
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
</h1>
<nav class="nav">
<c:if test="(not framesetp)">
<c:then>
<a href="/asteroid/">Home</a>
<a href="/asteroid/player">Player</a>
<a href="/asteroid/about">About</a>
<a href="/asteroid/status">Status</a>
<a href="/asteroid/profile" data-show-if-logged-in>Profile</a>
<a href="/asteroid/admin" data-show-if-admin>Admin</a>
<a href="/asteroid/login" data-show-if-logged-out>Login</a>
<a href="/asteroid/register" data-show-if-logged-out>Register</a>
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
</c:then>
<c:else>
<a href="/asteroid/content" target="_self">Home</a>
<a href="/asteroid/player-content" target="_self">Player</a>
<a href="/asteroid/about-content" target="_self">About</a>
<a href="/asteroid/status-content" target="_self">Status</a>
<a href="/asteroid/profile" target="_self" data-show-if-logged-in>Profile</a>
<a href="/asteroid/admin" target="_self" data-show-if-admin>Admin</a>
<a href="/asteroid/login" target="_self" data-show-if-logged-out>Login</a>
<a href="/asteroid/register" target="_self" data-show-if-logged-out>Register</a>
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
</c:else>
</c:if>
</nav>
<c:h>(asteroid::load-template "partial/navbar")</c:h>
</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;">🎵 Asteroid Music for Hackers</h2>

View File

@ -5,19 +5,13 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
<script src="/asteroid/static/js/auth-ui.js"></script>
<script lquery='(text (asteroid::get-auth-state-js-var))'></script>
<script src="/asteroid/static/js/admin.js"></script>
</head>
<body>
<div class="container">
<h1>🎛️ ADMIN DASHBOARD</h1>
<div class="nav">
<a href="/asteroid">Home</a>
<a href="/asteroid/player">Player</a>
<a href="/asteroid/profile">Profile</a>
<a href="/asteroid/admin/users">👥 Users</a>
<a href="/asteroid/logout" class="btn-logout">Logout</a>
</div>
<c:h>(asteroid::load-template "partial/navbar-admin")</c:h>
<!-- System Status -->
<div class="admin-section">

View File

@ -17,11 +17,7 @@
<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>
<c:h>(asteroid::load-template "partial/navbar")</c:h>
</header>
<main style="max-width: 800px; margin: 0 auto; padding: 20px;">
<section style="margin-bottom: 30px;">

View File

@ -8,7 +8,7 @@
<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">
<script src="/asteroid/static/js/auth-ui.js"></script>
<script lquery='(text (asteroid::get-auth-state-js-var))'></script>
<script src="/asteroid/static/js/front-page.js"></script>
<script src="/asteroid/static/js/recently-played.js"></script>
<script src="/api/asteroid/spectrum-analyzer.js"></script>
@ -24,7 +24,7 @@
<h3 class="page-subtitle">The Station at the End of Time</h3>
<!-- Spectrum Analyzer Canvas -->
<div style="text-align: center; margin: 15px 0;">
<div style="text-align: center; margin: 15px 0;" lquery='(css :display (when framesetp "none") :margin (when framesetp "0"))'>
<canvas id="spectrum-canvas" width="800" height="100" style="max-width: 100%; border: 1px solid #00ff00; background: #000; border-radius: 4px;"></canvas>
<div style="margin-top: 8px; font-size: 0.9em;">
<label style="margin-right: 10px;">
@ -50,32 +50,7 @@
</div>
</div>
<nav class="nav">
<c:if test="(not framesetp)">
<c:then>
<a href="/asteroid/">Home</a>
<a href="/asteroid/player">Player</a>
<a href="/asteroid/about">About</a>
<a href="/asteroid/status">Status</a>
<a href="/asteroid/profile" data-show-if-logged-in>Profile</a>
<a href="/asteroid/admin" data-show-if-admin>Admin</a>
<a href="/asteroid/login" data-show-if-logged-out>Login</a>
<a href="/asteroid/register" data-show-if-logged-out>Register</a>
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
</c:then>
<c:else>
<a href="/asteroid/content" target="_self">Home</a>
<a href="/asteroid/player-content" target="_self">Player</a>
<a href="/asteroid/about-content" target="_self">About</a>
<a href="/asteroid/status-content" target="_self">Status</a>
<a href="/asteroid/profile" target="_self" data-show-if-logged-in>Profile</a>
<a href="/asteroid/admin" target="_self" data-show-if-admin>Admin</a>
<a href="/asteroid/login" target="_self" data-show-if-logged-out>Login</a>
<a href="/asteroid/register" target="_self" data-show-if-logged-out>Register</a>
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
</c:else>
</c:if>
</nav>
<c:h>(asteroid::load-template "partial/navbar")</c:h>
</header>
<main>

View File

@ -16,12 +16,7 @@
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
<span>ASTEROID RADIO - LOGIN</span>
</h1>
<nav class="nav">
<a href="/asteroid">Home</a>
<a href="/asteroid/player">Player</a>
<a href="/asteroid/status">Status</a>
<a href="/asteroid/register">Register</a>
</nav>
<c:h>(asteroid::load-template "partial/navbar")</c:h>
</header>
<div class="auth-container">

View File

@ -0,0 +1,41 @@
<c:let home-href='(if framesetp "content" "")'
player-href='(if framesetp "player-content" "player")'
current-user='(asteroid::get-current-user)'
framesetp='framesetp'>
<!-- Navbar definition -->
<nav class="nav">
<c:unless test='(asteroid::member-string "home" (** :navbar-exclude))'>
<a lquery='(attr :href (eval (format nil "/asteroid/~a" home-href)) :target (when framesetp "_self"))'>
Home
</a>
</c:unless>
<c:unless test='(asteroid::member-string "player" (** :navbar-exclude))'>
<a lquery='(attr :href (eval (format nil "/asteroid/~a" player-href)) :target (when framesetp "_self"))'>
Player
</a>
</c:unless>
<c:unless test='(asteroid::member-string "profile" (** :navbar-exclude))'>
<a href="/asteroid/profile"
lquery='(attr :target (when framesetp "_self"))'>
Profile
</a>
</c:unless>
<c:unless test='(asteroid::member-string "admin" (** :navbar-exclude))'>
<a href="/asteroid/admin"
lquery='(attr :target (when framesetp "_self"))'>
Admin
</a>
</c:unless>
<c:unless test='(asteroid::member-string "users" (** :navbar-exclude))'>
<a href="/asteroid/admin/users"
lquery='(attr :target (when framesetp "_self"))'>
👥 Users
</a>
</c:unless>
<a href="/asteroid/logout"
lquery='(attr :target (when framesetp "_self"))'
class="btn-logout">
Logout
</a>
</nav>
</c:let>

View File

@ -0,0 +1,61 @@
<c:let home-href='(if framesetp "content" "")'
player-href='(if framesetp "player-content" "player")'
about-href='(if framesetp "about-content" "about")'
status-href='(if framesetp "status-content" "status")'
current-user='(asteroid::get-current-user)'
framesetp='framesetp'>
<!-- Navbar definition -->
<nav class="nav">
<c:unless test='(asteroid::member-string "home" (** :navbar-exclude))'>
<a lquery='(attr :href (eval (format nil "/asteroid/~a" home-href)) :target (when framesetp "_self"))'>
Home
</a>
</c:unless>
<c:unless test='(asteroid::member-string "player" (** :navbar-exclude))'>
<a lquery='(attr :href (eval (format nil "/asteroid/~a" player-href)) :target (when framesetp "_self"))'>
Player
</a>
</c:unless>
<c:unless test='(asteroid::member-string "about" (** :navbar-exclude))'>
<a lquery='(attr :href (eval (format nil "/asteroid/~a" about-href)) :target (when framesetp "_self"))'>
About
</a>
</c:unless>
<c:unless test='(asteroid::member-string "status" (** :navbar-exclude))'>
<a lquery='(attr :href (eval (format nil "/asteroid/~a" status-href)) :target (when framesetp "_self"))'>
Status
</a>
</c:unless>
<c:when test='(and current-user (not (asteroid::member-string "profile" (** :navbar-exclude))))'>
<a href="/asteroid/profile"
lquery='(attr :target (when framesetp "_self"))'>
Profile
</a>
</c:when>
<c:when test='(and (equal "admin" (clip current-user :role)) (not (asteroid::member-string "admin" (** :navbar-exclude))))'>
<a href="/asteroid/admin"
lquery='(attr :target (when framesetp "_self"))'>
Admin
</a>
</c:when>
<c:when test="(not current-user)">
<a href="/asteroid/login"
lquery='(attr :target (when framesetp "_self"))'>
Login
</a>
</c:when>
<c:when test="(not current-user)">
<a href="/asteroid/register"
lquery='(attr :target (when framesetp "_self"))'>
Register
</a>
</c:when>
<c:when test="current-user">
<a href="/asteroid/logout"
lquery='(attr :target (when framesetp "_self"))'
class="btn-logout">
Logout
</a>
</c:when>
</nav>
</c:let>

View File

@ -8,7 +8,7 @@
<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">
<script src="/asteroid/static/js/auth-ui.js"></script>
<script lquery='(text (asteroid::get-auth-state-js-var))'></script>
<script src="/asteroid/static/js/front-page.js"></script>
<script src="/asteroid/static/js/player.js"></script>
<c:if test="framesetp">
@ -28,7 +28,7 @@
<!-- Spectrum Analyzer Canvas -->
<c:if test="framesetp">
<c:then>
<div style="text-align: center; margin: 15px 0;">
<div style="text-align: center; margin: 15px 0; display: none;">
<canvas id="spectrum-canvas" width="800" height="100" style="max-width: 100%; border: 1px solid #00ff00; background: #000; border-radius: 4px;"></canvas>
<div style="margin-top: 8px; font-size: 0.9em;">
<label style="margin-right: 10px;">
@ -56,26 +56,7 @@
</c:then>
</c:if>
<div class="nav">
<c:if test="(not framesetp)">
<c:then>
<a href="/asteroid">Home</a>
<a href="/asteroid/profile">Profile</a>
<a href="/asteroid/admin" data-show-if-admin>Admin</a>
<a href="/asteroid/login" data-show-if-logged-out>Login</a>
<a href="/asteroid/register" data-show-if-logged-out>Register</a>
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
</c:then>
<c:else>
<a href="/asteroid/content" target="content-frame">Home</a>
<a href="/asteroid/profile" target="content-frame" data-show-if-logged-in>Profile</a>
<a href="/asteroid/admin" target="content-frame" data-show-if-admin>Admin</a>
<a href="/asteroid/login" target="content-frame" data-show-if-logged-out>Login</a>
<a href="/asteroid/register" target="content-frame" data-show-if-logged-out>Register</a>
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
</c:else>
</c:if>
</div>
<c:h>(asteroid::load-template "partial/navbar")</c:h>
<!-- Live Stream Section -->
<div class="player-section">

View File

@ -5,18 +5,13 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
<script src="/asteroid/static/js/auth-ui.js"></script>
<script lquery='(text (asteroid::get-auth-state-js-var))'></script>
<script src="/asteroid/static/js/profile.js"></script>
</head>
<body>
<div class="container">
<h1>👤 USER PROFILE</h1>
<div class="nav">
<a href="/asteroid">Home</a>
<a href="/asteroid/player">Player</a>
<a href="/asteroid/admin" data-show-if-admin>Admin</a>
<a href="/asteroid/logout" class="btn-logout">Logout</a>
</div>
<c:h>(asteroid::load-template "partial/navbar")</c:h>
<!-- User Profile Header -->
<div class="admin-section">

View File

@ -16,12 +16,7 @@
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
<span>ASTEROID RADIO - REGISTER</span>
</h1>
<nav class="nav">
<a href="/asteroid">Home</a>
<a href="/asteroid/player">Player</a>
<a href="/asteroid/status">Status</a>
<a href="/asteroid/login">Login</a>
</nav>
<c:h>(asteroid::load-template "partial/navbar")</c:h>
</header>
<div class="auth-container">

View File

@ -5,7 +5,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
<script src="/asteroid/static/js/auth-ui.js"></script>
<script lquery='(text (asteroid::get-auth-state-js-var))'></script>
</head>
<body>
<div class="container">
@ -16,33 +16,7 @@
<img src="/asteroid/static/asteroid.png" alt="Asteroid" style="height: 50px; width: auto;">
</h1>
<nav class="nav">
<c:if test="(not framesetp)">
<c:then>
<a href="/asteroid">Home</a>
<a href="/asteroid/player">Player</a>
<a href="/asteroid/about">About</a>
<a href="/asteroid/status">Status</a>
<a href="/asteroid/profile" data-show-if-logged-in>Profile</a>
<a href="/asteroid/admin" data-show-if-admin>Admin</a>
<a href="/asteroid/login" data-show-if-logged-out>Login</a>
<a href="/asteroid/register" data-show-if-logged-out>Register</a>
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
</c:then>
<c:else>
<a href="/asteroid/content" target="_self">Home</a>
<a href="/asteroid/player-content" target="_self">Player</a>
<a href="/asteroid/about-content" target="_self">About</a>
<a href="/asteroid/status-content" target="_self">Status</a>
<a href="/asteroid/profile" target="_self" data-show-if-logged-in>Profile</a>
<a href="/asteroid/admin" target="_self" data-show-if-admin>Admin</a>
<a href="/asteroid/login" target="_self" data-show-if-logged-out>Login</a>
<a href="/asteroid/register" target="_self" data-show-if-logged-out>Register</a>
<a href="/asteroid/logout" data-show-if-logged-in class="btn-logout">Logout</a>
</c:else>
</c:if>
</nav>
</header>
<c:h>(asteroid::load-template "partial/navbar")</c:h>
<main style="max-width: 800px; margin: 0 auto; padding: 20px;">
<section style="margin-bottom: 30px;">
@ -61,12 +35,14 @@
</ul>
</section>
<c:when test='(equal "admin" (clip (asteroid::get-current-user) :role))'>
<section style="margin-bottom: 30px;">
<h2 style="color: #00ff00; border-bottom: 2px solid #00ff00; padding-bottom: 10px;"> Additional Information</h2>
<p style="line-height: 1.6;">
For detailed system status and administration, please visit the <a href="/asteroid/admin" style="color: #00ff00;" data-show-if-admin>Admin Dashboard</a>.
For detailed system status and administration, please visit the <a href="/asteroid/admin" style="color: #00ff00;">Admin Dashboard</a>.
</p>
</section>
</c:when>
</main>
</div>
</body>

View File

@ -10,11 +10,7 @@
<body>
<div class="container">
<h1>👥 USER MANAGEMENT</h1>
<div class="nav">
<a href="/asteroid">Home</a>
<a href="/asteroid/admin">Admin</a>
<a href="/asteroid/logout" class="btn-logout">Logout</a>
</div>
<c:h>(asteroid::load-template "partial/navbar-admin")</c:h>
<!-- User Statistics -->
<div class="admin-section">

View File

@ -157,6 +157,17 @@
"Get the currently authenticated user's ID from session"
(session:field "user-id"))
(defun get-auth-state-js-var ()
"Builds a JavaScript variable definition with the current authentication state
for a request. The variable definition is a string ready to be injected in a
template file."
(let ((user (get-current-user)))
(format nil "var AUTHSTATE = ~a"
(cl-json:encode-json-to-string
`(("loggedIn" . ,(when user t))
("isAdmin" . ,(when (and user (user-has-role-p user :admin)) t))
("username" . ,(when user (dm:field user "username"))))))))
(defun require-authentication (&key (api nil))
"Require user to be authenticated.
Returns T if authenticated, NIL if not (after emitting error response).