From cc94bcb383bddc3cd50c11400e625bfeb4731571 Mon Sep 17 00:00:00 2001 From: Brian O'Reilly Date: Tue, 30 Sep 2025 12:50:45 -0400 Subject: [PATCH 1/8] Small moves. delete some binary data and add users.lisp --- music/library | 1 + music/library/Artist1/Album1/track1.mp3 | 1 - music/library/Artist2/Album2/track2.flac | 1 - music/library/test-song.mp3 | 1 - users.lisp | 2 ++ 5 files changed, 3 insertions(+), 3 deletions(-) create mode 120000 music/library delete mode 100644 music/library/Artist1/Album1/track1.mp3 delete mode 100644 music/library/Artist2/Album2/track2.flac delete mode 100644 music/library/test-song.mp3 create mode 100644 users.lisp diff --git a/music/library b/music/library new file mode 120000 index 0000000..20756bd --- /dev/null +++ b/music/library @@ -0,0 +1 @@ +/home/fade/Media/Music \ No newline at end of file diff --git a/music/library/Artist1/Album1/track1.mp3 b/music/library/Artist1/Album1/track1.mp3 deleted file mode 100644 index 7459abf..0000000 --- a/music/library/Artist1/Album1/track1.mp3 +++ /dev/null @@ -1 +0,0 @@ -dummy mp3 content diff --git a/music/library/Artist2/Album2/track2.flac b/music/library/Artist2/Album2/track2.flac deleted file mode 100644 index dae1b22..0000000 --- a/music/library/Artist2/Album2/track2.flac +++ /dev/null @@ -1 +0,0 @@ -dummy flac content diff --git a/music/library/test-song.mp3 b/music/library/test-song.mp3 deleted file mode 100644 index c2d1eba..0000000 --- a/music/library/test-song.mp3 +++ /dev/null @@ -1 +0,0 @@ -dummy audio content diff --git a/users.lisp b/users.lisp new file mode 100644 index 0000000..8466ca5 --- /dev/null +++ b/users.lisp @@ -0,0 +1,2 @@ +(in-package :asteroid) + From c5c687ec03a9b5c4cb4e47ac8da5086701618825 Mon Sep 17 00:00:00 2001 From: Brian O'Reilly Date: Sun, 14 Sep 2025 13:38:21 -0400 Subject: [PATCH 2/8] Some new dependencies start up a slynk server in the binary entry point so we can attach Sly to it and work live without pfaffing about in the threading library, hiding radiance from Sly/Slynk running inside emacs. --- asteroid.asd | 10 ++++++++-- asteroid.lisp | 5 ++++- database.lisp | 1 + 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/asteroid.asd b/asteroid.asd index 9f9a70a..8d1ea8e 100644 --- a/asteroid.asd +++ b/asteroid.asd @@ -9,7 +9,9 @@ :version "0.0.0" :defsystem-depends-on (:radiance) :class "radiance:virtual-module" - :depends-on (:radiance + :depends-on (:slynk + :radiance + :i-log4cl :r-clip :cl-json :dexador @@ -17,7 +19,11 @@ :r-data-model :cl-fad :local-time - :taglib) + :taglib + (:interface :database) + :r-data-model + (:interface :user)) + :pathname "./" :components ((:file "app-utils") (:file "module") diff --git a/asteroid.lisp b/asteroid.lisp index 4345a0c..7256537 100644 --- a/asteroid.lisp +++ b/asteroid.lisp @@ -310,9 +310,12 @@ (format t "~%Received interrupt, stopping server...~%") (stop-server)))) -(defun -main (&optional args) +(defun -main (&optional args (debug t)) (declare (ignorable args)) + (format t "~&args of asteroid: ~A~%" args) (format t "~%🎡 ASTEROID RADIO - Music for Hackers 🎡~%") (format t "Starting RADIANCE web server...~%") + (when debug + (slynk:create-server :port 4009 :dont-close t)) (run-server)) diff --git a/database.lisp b/database.lisp index 30be8c5..19d6abd 100644 --- a/database.lisp +++ b/database.lisp @@ -23,3 +23,4 @@ (track-ids :text)))) (format t "Database collections initialized~%")) + From c0bc316d6412e576a62922adda3ba5b8172b1e38 Mon Sep 17 00:00:00 2001 From: Brian O'Reilly Date: Sun, 14 Sep 2025 14:16:27 -0400 Subject: [PATCH 3/8] user profiles file. --- asteroid.asd | 1 + 1 file changed, 1 insertion(+) diff --git a/asteroid.asd b/asteroid.asd index 8d1ea8e..18c3b9a 100644 --- a/asteroid.asd +++ b/asteroid.asd @@ -29,4 +29,5 @@ (:file "module") (:file "database") (:file "stream-media") + (:file "users") (:file "asteroid"))) From 1778a269d873d2c66b21be9e1a0a549769de210c Mon Sep 17 00:00:00 2001 From: Brian O'Reilly Date: Sun, 14 Sep 2025 15:43:33 -0400 Subject: [PATCH 4/8] recursively scan the music directory to implicit depth 2 it is likely that the music library will contain directories of albums, read the files inside those dirs. --- asteroid.lisp | 2 ++ stream-media.lisp | 7 ++++++- template/admin.chtml | 6 +++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/asteroid.lisp b/asteroid.lisp index 7256537..d3f6624 100644 --- a/asteroid.lisp +++ b/asteroid.lisp @@ -285,7 +285,9 @@ ("listeners" . 0) ("stream-url" . "http://localhost:8000/asteroid")))) + ;; RADIANCE server management functions + (defun start-server (&key (port *server-port*)) "Start the Asteroid Radio RADIANCE server" (format t "Starting Asteroid Radio RADIANCE server on port ~a~%" port) diff --git a/stream-media.lisp b/stream-media.lisp index c1145b6..437c1c0 100644 --- a/stream-media.lisp +++ b/stream-media.lisp @@ -12,6 +12,11 @@ (remove-if-not #'supported-audio-file-p (cl-fad:list-directory directory :follow-symlinks nil)))) +(defun scan-directory-for-music-recursively (path) + (loop for directory in (uiop:subdirectories path) + with music = (scan-directory-for-music path) + appending (scan-directory-for-music directory))) + (defun extract-metadata-with-taglib (file-path) "Extract metadata using taglib library" (handler-case @@ -68,7 +73,7 @@ (defun scan-music-library (&optional (directory *music-library-path*)) "Scan music library directory and add tracks to database" (format t "Scanning music library: ~a~%" directory) - (let ((audio-files (scan-directory-for-music directory)) + (let ((audio-files (scan-directory-for-music-recursively directory)) (added-count 0)) (dolist (file audio-files) (let ((metadata (extract-metadata-with-taglib file))) diff --git a/template/admin.chtml b/template/admin.chtml index eca9611..67ea272 100644 --- a/template/admin.chtml +++ b/template/admin.chtml @@ -47,9 +47,9 @@

To add your own MP3 files:

    -
  1. Copy your MP3/FLAC/OGG/WAV files to: /home/glenn/Projects/Code/asteroid/music/incoming/
  2. -
  3. Click "Copy Files to Library" below
  4. -
  5. Files will be moved to the library and added to the database
  6. +
  7. Copy your MP3/FLAC/OGG/WAV files to: /home/glenn/Projects/Code/asteroid/music/incoming/
  8. +
  9. Click "Copy Files to Library" below
  10. +
  11. Files will be moved to the library and added to the database

Supported formats: MP3, FLAC, OGG, WAV

From 806031e57f4fc73e8201ce34189e548703d4a8cf Mon Sep 17 00:00:00 2001 From: Glenn Thompson Date: Tue, 16 Sep 2025 19:46:23 +0300 Subject: [PATCH 5/8] Fix Asteroid Radio authentication system - Fix database query syntax for RADIANCE hash table returns - Handle RADIANCE field storage format (lists instead of strings) - Configure r-simple-sessions module for session management - Update login page styling to match main site theme - Implement working authentication with admin/asteroid123 - Add proper error handling and debug logging - Ensure session persistence and redirects work correctly --- asteroid.asd | 15 +- asteroid.lisp | 81 ++++++++--- auth-routes.lisp | 143 +++++++++++++++++++ build-executable.lisp | 4 + database.lisp | 9 ++ static/asteroid.css | 290 +++++++++++++++++++++++++++++++++++++- static/asteroid.lass | 212 ++++++++++++++++++++++++++++ template/admin.chtml | 191 +++++++++++++++++++++++-- template/front-page.chtml | 72 +++++----- user-management.lisp | 218 ++++++++++++++++++++++++++++ 10 files changed, 1168 insertions(+), 67 deletions(-) create mode 100644 auth-routes.lisp create mode 100644 user-management.lisp diff --git a/asteroid.asd b/asteroid.asd index 18c3b9a..204db87 100644 --- a/asteroid.asd +++ b/asteroid.asd @@ -13,21 +13,22 @@ :radiance :i-log4cl :r-clip - :cl-json - :dexador :lass - :r-data-model - :cl-fad + :cl-json + :alexandria :local-time :taglib - (:interface :database) :r-data-model + :ironclad + :babel + :cl-fad + (:interface :database) (:interface :user)) - :pathname "./" :components ((:file "app-utils") (:file "module") (:file "database") (:file "stream-media") - (:file "users") + (:file "user-management") + (:file "auth-routes") (:file "asteroid"))) diff --git a/asteroid.lisp b/asteroid.lisp index d3f6624..481ebbf 100644 --- a/asteroid.lisp +++ b/asteroid.lisp @@ -21,9 +21,30 @@ (defparameter *supported-formats* '("mp3" "flac" "ogg" "wav")) +;; Authentication functions +(defun require-authentication () + "Require user to be authenticated" + (handler-case + (unless (session:field "user-id") + (radiance:redirect "/asteroid/login")) + (error (e) + (format t "Authentication error: ~a~%" e) + (radiance:redirect "/asteroid/login")))) + +(defun require-role (role) + "Require user to have a specific role" + (handler-case + (let ((current-user (get-current-user))) + (unless (and current-user (user-has-role-p current-user role)) + (radiance:redirect "/asteroid/login"))) + (error (e) + (format t "Role check error: ~a~%" e) + (radiance:redirect "/asteroid/login")))) + ;; API Routes (define-page admin-scan-library #@"/admin/scan-library" () "API endpoint to scan music library" + (require-role :admin) (handler-case (let ((tracks-added (scan-music-library))) (setf (radiance:header "Content-Type") "application/json") @@ -39,6 +60,7 @@ (define-page admin-tracks #@"/admin/tracks" () "API endpoint to view all tracks in database" + (require-authentication) (handler-case (let ((tracks (db:select "tracks" (db:query :all)))) (setf (radiance:header "Content-Type") "application/json") @@ -46,15 +68,13 @@ `(("status" . "success") ("tracks" . ,(mapcar (lambda (track) `(("id" . ,(gethash "_id" track)) - ("title" . ,(gethash "title" track)) - ("artist" . ,(gethash "artist" track)) - ("album" . ,(gethash "album" track)) - ("duration" . ,(gethash "duration" track)) - ("file-path" . ,(gethash "file-path" track)) - ("format" . ,(gethash "format" track)) - ("bitrate" . ,(gethash "bitrate" track)) - ("added-date" . ,(gethash "added-date" track)) - ("play-count" . ,(gethash "play-count" track)))) + ("title" . ,(first (gethash "title" track))) + ("artist" . ,(first (gethash "artist" track))) + ("album" . ,(first (gethash "album" track))) + ("duration" . ,(first (gethash "duration" track))) + ("format" . ,(first (gethash "format" track))) + ("bitrate" . ,(first (gethash "bitrate" track))) + ("play-count" . ,(first (gethash "play-count" track))))) tracks))))) (error (e) (setf (radiance:header "Content-Type") "application/json") @@ -220,13 +240,9 @@ `(("status" . "success") ("player" . ,(get-player-status))))) -;; Configure static file serving for other files -(define-page static #@"/static/(.*)" (:uri-groups (path)) - (serve-file (merge-pathnames (concatenate 'string "static/" path) - (asdf:system-source-directory :asteroid)))) - -;; RADIANCE route handlers -(define-page index #@"/" () +;; Front page +(define-page front-page #@"/" () + "Main front page" (let ((template-path (merge-pathnames "template/front-page.chtml" (asdf:system-source-directory :asteroid)))) (clip:process-to-string @@ -241,7 +257,15 @@ :now-playing-album "Startup Sounds" :now-playing-duration "∞"))) +;; Configure static file serving for other files +(define-page static #@"/static/(.*)" (:uri-groups (path)) + (serve-file (merge-pathnames (concatenate 'string "static/" path) + (asdf:system-source-directory :asteroid)))) + +;; Admin page (requires authentication) (define-page admin #@"/admin" () + "Admin dashboard" + (require-authentication) (let ((template-path (merge-pathnames "template/admin.chtml" (asdf:system-source-directory :asteroid))) (track-count (handler-case @@ -249,7 +273,7 @@ (error () 0)))) (clip:process-to-string (plump:parse (alexandria:read-file-into-string template-path)) - :title "Asteroid Radio - Admin Dashboard" + :title "🎡 ASTEROID RADIO - Admin Dashboard" :server-status "🟒 Running" :database-status (handler-case (if (db:connected-p) "🟒 Connected" "πŸ”΄ Disconnected") @@ -292,6 +316,11 @@ "Start the Asteroid Radio RADIANCE server" (format t "Starting Asteroid Radio RADIANCE server on port ~a~%" port) (compile-styles) ; Generate CSS file using LASS + + ;; Ensure RADIANCE environment is properly set before startup + (unless (radiance:environment) + (setf (radiance:environment) "default")) + (radiance:startup) (format t "Server started! Visit http://localhost:~a/asteroid/~%" port)) @@ -312,6 +341,17 @@ (format t "~%Received interrupt, stopping server...~%") (stop-server)))) +(defun ensure-radiance-environment () + "Ensure RADIANCE environment is properly configured for persistence" + (unless (radiance:environment) + (setf (radiance:environment) "default")) + + ;; Ensure the database directory exists + (let ((db-dir (merge-pathnames ".config/radiance/default/i-lambdalite/radiance.db/" + (user-homedir-pathname)))) + (ensure-directories-exist db-dir) + (format t "Database directory: ~a~%" db-dir))) + (defun -main (&optional args (debug t)) (declare (ignorable args)) (format t "~&args of asteroid: ~A~%" args) @@ -319,5 +359,12 @@ (format t "Starting RADIANCE web server...~%") (when debug (slynk:create-server :port 4009 :dont-close t)) + + ;; Ensure proper environment setup before starting + (ensure-radiance-environment) + + ;; Initialize user management before server starts + (initialize-user-system) + (run-server)) diff --git a/auth-routes.lisp b/auth-routes.lisp new file mode 100644 index 0000000..404f8d4 --- /dev/null +++ b/auth-routes.lisp @@ -0,0 +1,143 @@ +;;;; auth-routes.lisp - Authentication Routes for Asteroid Radio +;;;; Web routes for user authentication, registration, and management + +(in-package :asteroid) + +;; Login page (GET) +(define-page login #@"/login" () + "User login page" + (let ((username (radiance:post-var "username")) + (password (radiance:post-var "password"))) + (if (and username password) + ;; Handle login form submission + (let ((user (authenticate-user username password))) + (if user + (progn + ;; Login successful - store user ID in session + (format t "Login successful for user: ~a~%" (gethash "username" user)) + (handler-case + (progn + (let ((user-id (gethash "_id" user))) + (format t "User ID from DB: ~a~%" user-id) + (setf (session:field "user-id") (if (listp user-id) (first user-id) user-id))) + (radiance:redirect "/asteroid/admin")) + (error (e) + (format t "Session error: ~a~%" e) + "Login successful but session error occurred"))) + ;; Login failed - show form with error +" + + + Asteroid Radio - Login + + + +
+

🎡 ASTEROID RADIO - LOGIN

+
+
+

System Access

+
Invalid username or password
+
+
+ + +
+
+ + +
+
+ +
+
+
+ Default Admin Credentials:
+ Username: admin
+ Password: asteroid123 +
+
+
+
+ +")) + ;; Show login form (no POST data) +" + + + Asteroid Radio - Login + + + +
+

🎡 ASTEROID RADIO - LOGIN

+
+
+

System Access

+
+
+ + +
+
+ + +
+
+ +
+
+
+ Default Admin Credentials:
+ Username: admin
+ Password: asteroid123 +
+
+
+
+ +"))) + +;; Simple logout handler +(define-page logout #@"/logout" () + "Handle user logout" + (setf (session:field "user-id") nil) + (radiance:redirect "/asteroid/")) + +;; API: Get all users (admin only) +(define-page api-users #@"/api/users" () + "API endpoint to get all users" + (require-role :admin) + (setf (radiance:header "Content-Type") "application/json") + (handler-case + (let ((users (get-all-users))) + (cl-json:encode-json-to-string + `(("status" . "success") + ("users" . ,(mapcar (lambda (user) + `(("id" . ,(gethash "_id" user)) + ("username" . ,(gethash "username" user)) + ("email" . ,(gethash "email" user)) + ("role" . ,(gethash "role" user)) + ("active" . ,(gethash "active" user)) + ("created-date" . ,(gethash "created-date" user)) + ("last-login" . ,(gethash "last-login" user)))) + users))))) + (error (e) + (cl-json:encode-json-to-string + `(("status" . "error") + ("message" . ,(format nil "Error retrieving users: ~a" e))))))) + +;; API: Get user statistics (admin only) +(define-page api-user-stats #@"/api/user-stats" () + "API endpoint to get user statistics" + (require-role :admin) + (setf (radiance:header "Content-Type") "application/json") + (handler-case + (let ((stats (get-user-stats))) + (cl-json:encode-json-to-string + `(("status" . "success") + ("stats" . ,stats)))) + (error (e) + (cl-json:encode-json-to-string + `(("status" . "error") + ("message" . ,(format nil "Error retrieving user stats: ~a" e))))))) diff --git a/build-executable.lisp b/build-executable.lisp index 38a85c0..a4dd448 100755 --- a/build-executable.lisp +++ b/build-executable.lisp @@ -9,6 +9,10 @@ ;; Load RADIANCE first, then handle environment (ql:quickload :radiance) +;; Ensure RADIANCE environment is set before loading +(unless (radiance:environment) + (setf (radiance:environment) "default")) + ;; Load the system with RADIANCE environment handling (handler-bind ((radiance-core:environment-not-set (lambda (c) diff --git a/database.lisp b/database.lisp index 19d6abd..575a1c1 100644 --- a/database.lisp +++ b/database.lisp @@ -22,5 +22,14 @@ (created-date :integer) (track-ids :text)))) + (unless (db:collection-exists-p "USERS") + (db:create "USERS" '((username :text) + (email :text) + (password-hash :text) + (role :text) + (active :integer) + (created-date :integer) + (last-login :integer)))) + (format t "Database collections initialized~%")) diff --git a/static/asteroid.css b/static/asteroid.css index 66e57a6..834c22e 100644 --- a/static/asteroid.css +++ b/static/asteroid.css @@ -486,6 +486,294 @@ body input{ font-family: Courier New, monospace; } -body body.player-page{ +body .upload-interface{ + margin-top: 2rem; + padding: 1.5rem; + background-color: #1a1a1a; + border-radius: 8px; + border: 1px solid #333; +} + +body .upload-interface h3{ + color: #00ff00; + margin-bottom: 1rem; +} + +body .upload-interface .upload-area{ + border: 2px dashed #333; + border-radius: 8px; + padding: 2rem; text-align: center; + background-color: #0f0f0f; + -moz-transition: border-color 0.3s ease; + -o-transition: border-color 0.3s ease; + -webkit-transition: border-color 0.3s ease; + -ms-transition: border-color 0.3s ease; + transition: border-color 0.3s ease; +} + +body .upload-interface .upload-area &:hover{ + border-color: #00ff00; +} + +body .upload-interface .upload-area .upload-icon{ + font-size: 3rem; + color: #666; + margin-bottom: 1rem; +} + +body .upload-interface .upload-area p{ + color: #999; + margin-bottom: 1rem; +} + +body .upload-interface .upload-area .btn{ + margin-top: 1rem; +} + +body .auth-container{ + display: flex; + justify-content: center; + align-items: center; + min-height: 60vh; + padding: 2rem; +} + +body .auth-form{ + background-color: #1a1a1a; + border: 1px solid #333; + border-radius: 8px; + padding: 2rem; + width: 100%; + max-width: 400px; + -moz-box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + -o-box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + -webkit-box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + -ms-box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); +} + +body .auth-form h2{ + color: #00ff00; + text-align: center; + margin-bottom: 1.5rem; + font-size: 1.5rem; +} + +body .auth-form h3{ + color: #00ff00; + margin-bottom: 1rem; + font-size: 1.2rem; +} + +body .form-group{ + margin-bottom: 1rem; +} + +body .form-group label{ + display: block; + color: #ccc; + margin-bottom: 0.5rem; + font-weight: bold; +} + +body .form-group input{ + width: 100%; + padding: 0.75rem; + background-color: #0f0f0f; + border: 1px solid #333; + border-radius: 4px; + color: #fff; + font-size: 1rem; + box-sizing: border-box; +} + +body .form-group input &:focus{ + border-color: #00ff00; + outline: none; + -moz-box-shadow: 0 0 0 2px rgba(0, 255, 0, 0.2); + -o-box-shadow: 0 0 0 2px rgba(0, 255, 0, 0.2); + -webkit-box-shadow: 0 0 0 2px rgba(0, 255, 0, 0.2); + -ms-box-shadow: 0 0 0 2px rgba(0, 255, 0, 0.2); + box-shadow: 0 0 0 2px rgba(0, 255, 0, 0.2); +} + +body .form-actions{ + display: flex; + gap: 1rem; + margin-top: 1.5rem; +} + +body .message{ + padding: 0.75rem; + border-radius: 4px; + margin-top: 1rem; + text-align: center; + font-weight: bold; +} + +body .message &.success{ + background-color: rgba(0, 255, 0, 0.1); + border: 1px solid #00ff00; + color: #00ff00; +} + +body .message &.error{ + background-color: rgba(255, 0, 0, 0.1); + border: 1px solid #ff0000; + color: #ff0000; +} + +body .auth-link{ + text-align: center; + margin-top: 1.5rem; + color: #999; +} + +body .auth-link a{ + color: #00ff00; + text-decoration: none; +} + +body .auth-link a &:hover{ + text-decoration: underline; +} + +body .profile-container{ + max-width: 600px; + margin: 2rem auto; + padding: 0 1rem; +} + +body .profile-card{ + background-color: #1a1a1a; + border: 1px solid #333; + border-radius: 8px; + padding: 2rem; + margin-bottom: 2rem; +} + +body .profile-card h2{ + color: #00ff00; + margin-bottom: 1.5rem; + text-align: center; +} + +body .profile-info{ + margin-bottom: 2rem; +} + +body .info-group{ + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 0; + border-bottom: 1px solid #333; +} + +body .info-group &:last-child{ + border-bottom: none; +} + +body .info-group label{ + color: #ccc; + font-weight: bold; +} + +body .info-group span{ + color: #fff; +} + +body .role-badge{ + background-color: #00ff00; + color: #000; + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.875rem; + font-weight: bold; +} + +body .profile-actions{ + display: flex; + gap: 1rem; + justify-content: center; +} + +body .user-management{ + margin-top: 2rem; +} + +body .users-table{ + width: 100%; + border-collapse: collapse; + background-color: #1a1a1a; + border: 1px solid #333; + border-radius: 8px; + overflow: hidden; +} + +body .users-table thead{ + background-color: #0f0f0f; +} + +body .users-table thead th{ + padding: 1rem; + text-align: left; + color: #00ff00; + font-weight: bold; + border-bottom: 1px solid #333; +} + + + +body .users-table tbody tr{ + border-bottom: 1px solid #333; +} + +body .users-table tbody tr &:hover{ + background-color: #222; +} + +body .users-table tbody tr td{ + padding: 1rem; + color: #fff; + vertical-align: middle; +} + +body .users-table tbody .user-actions{ + display: flex; + gap: 0.5rem; +} + +body .users-table tbody .user-actions .btn{ + padding: 0.25rem 0.5rem; + font-size: 0.875rem; +} + +body .user-stats{ + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1rem; + margin-bottom: 2rem; +} + +body .stat-card{ + background-color: #1a1a1a; + border: 1px solid #333; + border-radius: 8px; + padding: 1rem; + text-align: center; +} + +body .stat-card .stat-number{ + font-size: 2rem; + font-weight: bold; + color: #00ff00; + display: block; +} + +body .stat-card .stat-label{ + color: #ccc; + font-size: 0.875rem; + margin-top: 0.5rem; } \ No newline at end of file diff --git a/static/asteroid.lass b/static/asteroid.lass index 165e090..0e9e9eb 100644 --- a/static/asteroid.lass +++ b/static/asteroid.lass @@ -384,6 +384,218 @@ :border-radius "3px" :font-family "Courier New, monospace") + (.upload-interface + :margin-top 2rem + :padding 1.5rem + :background-color "#1a1a1a" + :border-radius 8px + :border "1px solid #333" + + (h3 :color "#00ff00" + :margin-bottom 1rem) + + (.upload-area + :border "2px dashed #333" + :border-radius 8px + :padding 2rem + :text-align center + :background-color "#0f0f0f" + :transition "border-color 0.3s ease" + + ("&:hover" :border-color "#00ff00") + + (.upload-icon :font-size 3rem + :color "#666" + :margin-bottom 1rem) + + (p :color "#999" + :margin-bottom 1rem) + + (.btn :margin-top 1rem))) + + ;; Authentication Styles + (.auth-container + :display flex + :justify-content center + :align-items center + :min-height "60vh" + :padding 2rem) + + (.auth-form + :background-color "#1a1a1a" + :border "1px solid #333" + :border-radius 8px + :padding 2rem + :width "100%" + :max-width 400px + :box-shadow "0 4px 6px rgba(0, 0, 0, 0.3)" + + (h2 :color "#00ff00" + :text-align center + :margin-bottom 1.5rem + :font-size 1.5rem) + + (h3 :color "#00ff00" + :margin-bottom 1rem + :font-size 1.2rem)) + + (.form-group + :margin-bottom 1rem + + (label :display block + :color "#ccc" + :margin-bottom 0.5rem + :font-weight bold) + + (input :width "100%" + :padding 0.75rem + :background-color "#0f0f0f" + :border "1px solid #333" + :border-radius 4px + :color "#fff" + :font-size 1rem + :box-sizing border-box + + ("&:focus" :border-color "#00ff00" + :outline none + :box-shadow "0 0 0 2px rgba(0, 255, 0, 0.2)"))) + + (.form-actions + :display flex + :gap 1rem + :margin-top 1.5rem) + + (.message + :padding 0.75rem + :border-radius 4px + :margin-top 1rem + :text-align center + :font-weight bold + + ("&.success" :background-color "rgba(0, 255, 0, 0.1)" + :border "1px solid #00ff00" + :color "#00ff00") + + ("&.error" :background-color "rgba(255, 0, 0, 0.1)" + :border "1px solid #ff0000" + :color "#ff0000")) + + (.auth-link + :text-align center + :margin-top 1.5rem + :color "#999" + + (a :color "#00ff00" + :text-decoration none + + ("&:hover" :text-decoration underline))) + + ;; Profile Styles + (.profile-container + :max-width 600px + :margin "2rem auto" + :padding 0 1rem) + + (.profile-card + :background-color "#1a1a1a" + :border "1px solid #333" + :border-radius 8px + :padding 2rem + :margin-bottom 2rem + + (h2 :color "#00ff00" + :margin-bottom 1.5rem + :text-align center)) + + (.profile-info + :margin-bottom 2rem) + + (.info-group + :display flex + :justify-content space-between + :align-items center + :padding 0.75rem 0 + :border-bottom "1px solid #333" + + ("&:last-child" :border-bottom none) + + (label :color "#ccc" + :font-weight bold) + + (span :color "#fff")) + + (.role-badge + :background-color "#00ff00" + :color "#000" + :padding "0.25rem 0.5rem" + :border-radius 4px + :font-size 0.875rem + :font-weight bold) + + (.profile-actions + :display flex + :gap 1rem + :justify-content center) + + ;; User Management Styles + (.user-management + :margin-top 2rem) + + (.users-table + :width "100%" + :border-collapse collapse + :background-color "#1a1a1a" + :border "1px solid #333" + :border-radius 8px + :overflow hidden + + (thead + :background-color "#0f0f0f" + + (th :padding 1rem + :text-align left + :color "#00ff00" + :font-weight bold + :border-bottom "1px solid #333")) + + (tbody + (tr :border-bottom "1px solid #333" + + ("&:hover" :background-color "#222") + + (td :padding 1rem + :color "#fff" + :vertical-align middle)) + + (.user-actions + :display flex + :gap 0.5rem + + (.btn :padding "0.25rem 0.5rem" + :font-size 0.875rem)))) + + (.user-stats + :display grid + :grid-template-columns "repeat(auto-fit, minmax(150px, 1fr))" + :gap 1rem + :margin-bottom 2rem) + + (.stat-card + :background-color "#1a1a1a" + :border "1px solid #333" + :border-radius 8px + :padding 1rem + :text-align center + + (.stat-number :font-size 2rem + :font-weight bold + :color "#00ff00" + :display block) + + (.stat-label :color "#ccc" + :font-size 0.875rem + :margin-top 0.5rem))) + ;; Center alignment for player page (body.player-page :text-align center)) diff --git a/template/admin.chtml b/template/admin.chtml index 67ea272..ee844a5 100644 --- a/template/admin.chtml +++ b/template/admin.chtml @@ -91,17 +91,44 @@

Player Control

-
- - - - +
+

🎡 Player Control

+
+ + + + +
+
+ Status: Unknown
+ Current Track: None +
- -
-

Status: Stopped

-

Current Track: None

-

Position: 0s

+ +
+

πŸ‘₯ User Management

+
+
+ 0 + Total Users +
+
+ 0 + Active Users +
+
+ 0 + Admins +
+
+ 0 + DJs +
+
+
+ + +
@@ -325,8 +352,152 @@ alert('Copy your MP3 files to: /home/glenn/Projects/Code/asteroid/music/incoming/\n\nThen click "Copy Files to Library" to add them to your music collection.'); } + // User Management Functions + async function loadUserStats() { + try { + const response = await fetch('/asteroid/api/users/stats'); + const result = await response.json(); + + if (result.status === 'success') { + const stats = result.stats; + document.getElementById('total-users').textContent = stats.total; + document.getElementById('active-users').textContent = stats.active; + document.getElementById('admin-users').textContent = stats.admins; + document.getElementById('dj-users').textContent = stats.djs; + } + } catch (error) { + console.error('Error loading user stats:', error); + } + } + + async function loadUsers() { + try { + const response = await fetch('/asteroid/api/users'); + const result = await response.json(); + + if (result.status === 'success') { + showUsersTable(result.users); + } + } catch (error) { + console.error('Error loading users:', error); + alert('Error loading users. Please try again.'); + } + } + + function showUsersTable(users) { + const container = document.createElement('div'); + container.className = 'user-management'; + container.innerHTML = ` +

User Management

+ + + + + + + + + + + + + ${users.map(user => ` + + + + + + + + + `).join('')} + +
UsernameEmailRoleStatusLast LoginActions
${user.username}${user.email} + + ${user.active ? 'βœ… Active' : '❌ Inactive'}${user['last-login'] ? new Date(user['last-login'] * 1000).toLocaleString() : 'Never'}
+ + `; + + document.body.appendChild(container); + } + + function hideUsersTable() { + const userManagement = document.querySelector('.user-management'); + if (userManagement) { + userManagement.remove(); + } + } + + async function updateUserRole(userId, newRole) { + try { + const formData = new FormData(); + formData.append('role', newRole); + + const response = await fetch(`/asteroid/api/users/${userId}/role`, { + method: 'POST', + body: formData + }); + + const result = await response.json(); + + if (result.status === 'success') { + loadUserStats(); + alert('User role updated successfully'); + } else { + alert('Error updating user role: ' + result.message); + } + } catch (error) { + console.error('Error updating user role:', error); + alert('Error updating user role. Please try again.'); + } + } + + async function deactivateUser(userId) { + if (!confirm('Are you sure you want to deactivate this user?')) { + return; + } + + try { + const response = await fetch(`/asteroid/api/users/${userId}/deactivate`, { + method: 'POST' + }); + + const result = await response.json(); + + if (result.status === 'success') { + loadUsers(); + loadUserStats(); + alert('User deactivated successfully'); + } else { + alert('Error deactivating user: ' + result.message); + } + } catch (error) { + console.error('Error deactivating user:', error); + alert('Error deactivating user. Please try again.'); + } + } + + function showCreateUser() { + window.location.href = '/asteroid/register'; + } + + // Load user stats on page load + loadUserStats(); + // Update player status every 5 seconds setInterval(updatePlayerStatus, 5000); + + // Update user stats every 30 seconds + setInterval(loadUserStats, 30000); diff --git a/template/front-page.chtml b/template/front-page.chtml index 84af182..4754e5f 100644 --- a/template/front-page.chtml +++ b/template/front-page.chtml @@ -4,41 +4,49 @@ 🎡 ASTEROID RADIO 🎡 - +
-

🎡 ASTEROID RADIO 🎡

-
-

Station Status

-

🟒 LIVE - Broadcasting asteroid music for hackers

-

Current listeners: 0

-

Stream quality: 128kbps MP3

-
- - -
-

πŸ”΄ LIVE STREAM

-

Stream URL: http://localhost:8000/asteroid.mp3

-

Format: MP3 128kbps Stereo

-

Status: ● BROADCASTING

- -
-
-

Now Playing

-

Artist: The Void

-

Track: Silence

-

Album: Startup Sounds

-

Duration: ∞

-
+
+

🎡 ASTEROID RADIO 🎡

+ +
+ +
+
+

Station Status

+

🟒 LIVE - Broadcasting asteroid music for hackers

+

Current listeners: 0

+

Stream quality: 128kbps MP3

+
+ +
+

πŸ”΄ LIVE STREAM

+

Stream URL: http://localhost:8000/asteroid.mp3

+

Format: MP3 128kbps Stereo

+

Status: ● BROADCASTING

+ +
+ +
+

Now Playing

+

Artist: The Void

+

Track: Silence

+

Album: Startup Sounds

+

Duration: ∞

+
+
diff --git a/user-management.lisp b/user-management.lisp new file mode 100644 index 0000000..10c6d04 --- /dev/null +++ b/user-management.lisp @@ -0,0 +1,218 @@ +;;;; user-management.lisp - User Management System for Asteroid Radio +;;;; Core user management functionality and database operations + +(in-package :asteroid) + +;; User roles and permissions +(defparameter *user-roles* '(:listener :dj :admin)) + +;; User management functions +(defun create-user (username email password &key (role :listener) (active t)) + "Create a new user account" + (let* ((password-hash (hash-password password)) + (user-data `(("username" ,username) + ("email" ,email) + ("password-hash" ,password-hash) + ("role" ,(string-downcase (symbol-name role))) + ("active" ,(if active 1 0)) + ("created-date" ,(local-time:timestamp-to-unix (local-time:now))) + ("last-login" nil)))) + (handler-case + (db:with-transaction () + (format t "Inserting user data: ~a~%" user-data) + (let ((result (db:insert "USERS" user-data))) + (format t "Insert result: ~a~%" result) + (format t "User created: ~a (~a)~%" username role) + t)) + (error (e) + (format t "Error creating user ~a: ~a~%" username e) + nil)))) + +(defun find-user-by-username (username) + "Find a user by username" + (format t "Searching for user: ~a~%" username) + (format t "Available collections: ~a~%" (db:collections)) + (format t "Trying to select from USERS collection...~%") + (let ((all-users-test (db:select "USERS" (db:query :all)))) + (format t "Total users in USERS collection: ~a~%" (length all-users-test)) + (dolist (user all-users-test) + (format t "User data: ~a~%" user) + (format t "Username field: ~a~%" (gethash "username" user)))) + (let ((all-users (db:select "USERS" (db:query :all))) + (users nil)) + (dolist (user all-users) + (format t "Comparing ~a with ~a~%" (gethash "username" user) username) + (when (equal (first (gethash "username" user)) username) + (push user users))) + (format t "Query returned ~a users~%" (length users)) + (when users + (format t "First user: ~a~%" (first users)) + (first users)))) + +(defun find-user-by-id (user-id) + "Find a user by ID" + (let ((users (db:select "USERS" (db:query (:= "_id" user-id))))) + (when users (first users)))) + +(defun authenticate-user (username password) + "Authenticate a user with username and password" + (format t "Attempting to authenticate user: ~a~%" username) + (let ((user (find-user-by-username username))) + (format t "User found: ~a~%" (if user "YES" "NO")) + (when user + (handler-case + (progn + (format t "User active: ~a~%" (gethash "active" user)) + (format t "Password hash from DB: ~a~%" (gethash "password-hash" user)) + (format t "Password verification: ~a~%" + (verify-password password (first (gethash "password-hash" user))))) + (error (e) + (format t "Error during user data access: ~a~%" e)))) + (when (and user + (= (first (gethash "active" user)) 1) + (verify-password password (first (gethash "password-hash" user)))) + ;; Update last login + (db:update "USERS" + (db:query (:= "_id" (gethash "_id" user))) + `(("last-login" ,(local-time:timestamp-to-unix (local-time:now))))) + user))) + +(defun hash-password (password) + "Hash a password using ironclad" + (let ((digest (ironclad:make-digest :sha256))) + (ironclad:update-digest digest (babel:string-to-octets password)) + (ironclad:byte-array-to-hex-string (ironclad:produce-digest digest)))) + +(defun verify-password (password hash) + "Verify a password against its hash" + (string= (hash-password password) hash)) + +(defun user-has-role-p (user role) + "Check if user has a specific role" + (when user + (let ((user-role (intern (string-upcase (gethash "role" user)) :keyword))) + (or (eq user-role role) + (and (eq role :listener) (member user-role '(:dj :admin))) + (and (eq role :dj) (eq user-role :admin)))))) + +(defun get-current-user () + "Get the currently authenticated user from session" + (handler-case + (let ((user-id (session:field "user-id"))) + (when user-id + (find-user-by-id user-id))) + (error (e) + (format t "Error getting current user: ~a~%" e) + nil))) + +(defun require-authentication () + "Require user to be authenticated" + (handler-case + (unless (session:field "user-id") + (radiance:redirect "/asteroid/login")) + (error (e) + (format t "Authentication error: ~a~%" e) + (radiance:redirect "/asteroid/login")))) + +(defun require-role (role) + "Require user to have a specific role" + (handler-case + (let ((current-user (get-current-user))) + (unless (and current-user (user-has-role-p current-user role)) + (radiance:redirect "/asteroid/login"))) + (error (e) + (format t "Role check error: ~a~%" e) + (radiance:redirect "/asteroid/login")))) + +(defun update-user-role (user-id new-role) + "Update a user's role" + (handler-case + (progn + (db:update "USERS" + (db:query (:= "_id" user-id)) + `(("role" ,(string-downcase (symbol-name new-role))))) + (format t "Updated user ~a role to ~a~%" user-id new-role) + t) + (error (e) + (format t "Error updating user role: ~a~%" e) + nil))) + +(defun deactivate-user (user-id) + "Deactivate a user account" + (handler-case + (progn + (db:update "USERS" + (db:query (:= "_id" user-id)) + `(("active" 0))) + (format t "Deactivated user ~a~%" user-id) + t) + (error (e) + (format t "Error deactivating user: ~a~%" e) + nil))) + +(defun activate-user (user-id) + "Activate a user account" + (handler-case + (progn + (db:update "USERS" + (db:query (:= "_id" user-id)) + `(("active" 1))) + (format t "Activated user ~a~%" user-id) + t) + (error (e) + (format t "Error activating user: ~a~%" e) + nil))) + +(defun get-all-users () + "Get all users from database" + (format t "Getting all users from database...~%") + (let ((all-users (db:select "USERS" (db:query :all)))) + (format t "Total users in database: ~a~%" (length all-users)) + (dolist (user all-users) + (format t "User: ~a~%" user)) + all-users)) + +(defun get-user-stats () + "Get user statistics" + (let ((all-users (get-all-users))) + `(("total-users" . ,(length all-users)) + ("active-users" . ,(count-if (lambda (user) (gethash "active" user)) all-users)) + ("listeners" . ,(count-if (lambda (user) (string= (gethash "role" user) "listener")) all-users)) + ("djs" . ,(count-if (lambda (user) (string= (gethash "role" user) "dj")) all-users)) + ("admins" . ,(count-if (lambda (user) (string= (gethash "role" user) "admin")) all-users))))) + +(defun create-default-admin () + "Create default admin user if no admin exists" + (let ((existing-admins (remove-if-not + (lambda (user) (string= (gethash "role" user) "admin")) + (get-all-users)))) + (unless existing-admins + (format t "~%Creating default admin user...~%") + (format t "Username: admin~%") + (format t "Password: asteroid123~%") + (format t "Please change this password after first login!~%~%") + (create-user "admin" "admin@asteroid.radio" "asteroid123" :role :admin :active t)))) + +(defun initialize-user-system () + "Initialize the user management system" + (format t "Initializing user management system...~%") + ;; Try immediate initialization first + (handler-case + (progn + (format t "Setting up user management...~%") + (create-default-admin) + (format t "User management initialization complete.~%")) + (error (e) + (format t "Database not ready, will retry in background: ~a~%" e) + ;; Fallback to delayed initialization + (bt:make-thread + (lambda () + (sleep 3) ; Give database more time to initialize + (handler-case + (progn + (format t "Retrying user management setup...~%") + (create-default-admin) + (format t "User management initialization complete.~%")) + (error (e) + (format t "Error initializing user system: ~a~%" e)))) + :name "user-init")))) From 00942b60bc65daf4ed315f1706230da20e8d5f3a Mon Sep 17 00:00:00 2001 From: Glenn Thompson Date: Tue, 16 Sep 2025 21:45:58 +0300 Subject: [PATCH 6/8] Fix user management API authentication and data formatting - Fixed find-user-by-id to handle BIT type database IDs - Updated user-has-role-p to extract role from list format - Enhanced API endpoint to return properly formatted JSON data - Added comprehensive debugging for authentication flow - Created login.chtml template with CLIP data binding - Resolved 'Error loading users' issue in admin panel --- auth-routes.lisp | 101 ++++++++++--------------------------------- template/login.chtml | 40 +++++++++++++++++ user-management.lisp | 40 ++++++++++++----- 3 files changed, 92 insertions(+), 89 deletions(-) create mode 100644 template/login.chtml diff --git a/auth-routes.lisp b/auth-routes.lisp index 404f8d4..83e427b 100644 --- a/auth-routes.lisp +++ b/auth-routes.lisp @@ -7,7 +7,9 @@ (define-page login #@"/login" () "User login page" (let ((username (radiance:post-var "username")) - (password (radiance:post-var "password"))) + (password (radiance:post-var "password")) + (template-path (merge-pathnames "template/login.chtml" + (asdf:system-source-directory :asteroid)))) (if (and username password) ;; Handle login form submission (let ((user (authenticate-user username password))) @@ -25,78 +27,17 @@ (format t "Session error: ~a~%" e) "Login successful but session error occurred"))) ;; Login failed - show form with error -" - - - Asteroid Radio - Login - - - -
-

🎡 ASTEROID RADIO - LOGIN

-
-
-

System Access

-
Invalid username or password
-
-
- - -
-
- - -
-
- -
-
-
- Default Admin Credentials:
- Username: admin
- Password: asteroid123 -
-
-
-
- -")) + (clip:process-to-string + (plump:parse (alexandria:read-file-into-string template-path)) + :title "Asteroid Radio - Login" + :error-message "Invalid username or password" + :display-error "display: block;"))) ;; Show login form (no POST data) -" - - - Asteroid Radio - Login - - - -
-

🎡 ASTEROID RADIO - LOGIN

-
-
-

System Access

-
-
- - -
-
- - -
-
- -
-
-
- Default Admin Credentials:
- Username: admin
- Password: asteroid123 -
-
-
-
- -"))) + (clip:process-to-string + (plump:parse (alexandria:read-file-into-string template-path)) + :title "Asteroid Radio - Login" + :error-message "" + :display-error "display: none;")))) ;; Simple logout handler (define-page logout #@"/logout" () @@ -114,13 +55,15 @@ (cl-json:encode-json-to-string `(("status" . "success") ("users" . ,(mapcar (lambda (user) - `(("id" . ,(gethash "_id" user)) - ("username" . ,(gethash "username" user)) - ("email" . ,(gethash "email" user)) - ("role" . ,(gethash "role" user)) - ("active" . ,(gethash "active" user)) - ("created-date" . ,(gethash "created-date" user)) - ("last-login" . ,(gethash "last-login" user)))) + `(("id" . ,(if (listp (gethash "_id" user)) + (first (gethash "_id" user)) + (gethash "_id" user))) + ("username" . ,(first (gethash "username" user))) + ("email" . ,(first (gethash "email" user))) + ("role" . ,(first (gethash "role" user))) + ("active" . ,(= (first (gethash "active" user)) 1)) + ("created-date" . ,(first (gethash "created-date" user))) + ("last-login" . ,(first (gethash "last-login" user))))) users))))) (error (e) (cl-json:encode-json-to-string diff --git a/template/login.chtml b/template/login.chtml new file mode 100644 index 0000000..ef45cd1 --- /dev/null +++ b/template/login.chtml @@ -0,0 +1,40 @@ + + + + Asteroid Radio - Login + + + + + +
+

🎡 ASTEROID RADIO - LOGIN

+
+
+

System Access

+
+ Invalid username or password +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ Default Admin Credentials:
+ Username: admin
+ Password: asteroid123 +
+
+
+
+ + diff --git a/user-management.lisp b/user-management.lisp index 10c6d04..7704773 100644 --- a/user-management.lisp +++ b/user-management.lisp @@ -51,8 +51,17 @@ (defun find-user-by-id (user-id) "Find a user by ID" - (let ((users (db:select "USERS" (db:query (:= "_id" user-id))))) - (when users (first users)))) + (format t "Looking for user with ID: ~a (type: ~a)~%" user-id (type-of user-id)) + ;; Handle both integer and BIT types by iterating through all users + (let ((all-users (db:select "USERS" (db:query :all))) + (target-id (if (numberp user-id) user-id (parse-integer (format nil "~a" user-id))))) + (format t "Searching through ~a users for ID ~a~%" (length all-users) target-id) + (dolist (user all-users) + (let ((db-id (gethash "_id" user))) + (format t "Checking user with _id: ~a (type: ~a)~%" db-id (type-of db-id)) + (when (equal db-id target-id) + (format t "Found matching user!~%") + (return user)))))) (defun authenticate-user (username password) "Authenticate a user with username and password" @@ -88,9 +97,12 @@ (string= (hash-password password) hash)) (defun user-has-role-p (user role) - "Check if user has a specific role" + "Check if user has the specified role" (when user - (let ((user-role (intern (string-upcase (gethash "role" user)) :keyword))) + (let* ((role-field (gethash "role" user)) + (role-string (if (listp role-field) (first role-field) role-field)) + (user-role (intern (string-upcase role-string) :keyword))) + (format t "User role: ~a, checking against: ~a~%" user-role role) (or (eq user-role role) (and (eq role :listener) (member user-role '(:dj :admin))) (and (eq role :dj) (eq user-role :admin)))))) @@ -99,8 +111,11 @@ "Get the currently authenticated user from session" (handler-case (let ((user-id (session:field "user-id"))) + (format t "Session user-id: ~a~%" user-id) (when user-id - (find-user-by-id user-id))) + (let ((user (find-user-by-id user-id))) + (format t "Found user: ~a~%" (if user "YES" "NO")) + user))) (error (e) (format t "Error getting current user: ~a~%" e) nil))) @@ -118,7 +133,11 @@ "Require user to have a specific role" (handler-case (let ((current-user (get-current-user))) + (format t "Current user for role check: ~a~%" (if current-user "FOUND" "NOT FOUND")) + (when current-user + (format t "User has role ~a: ~a~%" role (user-has-role-p current-user role))) (unless (and current-user (user-has-role-p current-user role)) + (format t "Role check failed - redirecting to login~%") (radiance:redirect "/asteroid/login"))) (error (e) (format t "Role check error: ~a~%" e) @@ -166,11 +185,12 @@ (defun get-all-users () "Get all users from database" (format t "Getting all users from database...~%") - (let ((all-users (db:select "USERS" (db:query :all)))) - (format t "Total users in database: ~a~%" (length all-users)) - (dolist (user all-users) - (format t "User: ~a~%" user)) - all-users)) + (let ((users (db:select "USERS" (db:query :all)))) + (format t "Total users in database: ~a~%" (length users)) + (dolist (user users) + (format t "User: ~a~%" user) + (format t "User _id field: ~a (type: ~a)~%" (gethash "_id" user) (type-of (gethash "_id" user)))) + users)) (defun get-user-stats () "Get user statistics" From d60b73c42445b8411b52c1b60899d747e5b70219 Mon Sep 17 00:00:00 2001 From: Glenn Thompson Date: Tue, 16 Sep 2025 21:48:29 +0300 Subject: [PATCH 7/8] Add bordeaux-threads dependency to asteroid.asd - Required for threading functionality in the authentication system --- asteroid.asd | 1 + 1 file changed, 1 insertion(+) diff --git a/asteroid.asd b/asteroid.asd index 204db87..8f1d9e1 100644 --- a/asteroid.asd +++ b/asteroid.asd @@ -22,6 +22,7 @@ :ironclad :babel :cl-fad + :bordeaux-threads (:interface :database) (:interface :user)) :pathname "./" From 6b1b330ed289add7e2c0a286803fcd52cc23d398 Mon Sep 17 00:00:00 2001 From: Glenn Thompson Date: Tue, 30 Sep 2025 21:53:49 +0300 Subject: [PATCH 8/8] fix: Correct template paths and navigation links - Fix CSS paths in admin.chtml, login.chtml, and player.chtml (change /static/ to /asteroid/static/) - Fix navigation links to use correct /asteroid/ prefix - Fix player link to include trailing slash (/asteroid/player/) - Resolves 'layout fuckage' issues in SystemConfiguration branch All templates now properly load CSS and navigation works correctly. --- template/admin.chtml | 6 +++--- template/front-page.chtml | 2 +- template/login.chtml | 2 +- template/player.chtml | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/template/admin.chtml b/template/admin.chtml index ee844a5..25dec38 100644 --- a/template/admin.chtml +++ b/template/admin.chtml @@ -4,14 +4,14 @@ Asteroid Radio - Admin Dashboard - +

πŸŽ›οΈ ADMIN DASHBOARD

diff --git a/template/front-page.chtml b/template/front-page.chtml index 4754e5f..0a5ba50 100644 --- a/template/front-page.chtml +++ b/template/front-page.chtml @@ -12,7 +12,7 @@

🎡 ASTEROID RADIO 🎡