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
+
+
+
+"))
+ ;; Show login form (no POST data)
+"
+
+
+ Asteroid Radio - Login
+
+
+
+
+
π΅ ASTEROID RADIO - LOGIN
+
+
+
+")))
+
+;; 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
+
+
+
+ | Username |
+ Email |
+ Role |
+ Status |
+ Last Login |
+ Actions |
+
+
+
+ ${users.map(user => `
+
+ | ${user.username} |
+ ${user.email} |
+
+
+ |
+ ${user.active ? 'β
Active' : 'β Inactive'} |
+ ${user['last-login'] ? new Date(user['last-login'] * 1000).toLocaleString() : 'Never'} |
+
+ ${user.active ?
+ `` :
+ ``
+ }
+ |
+
+ `).join('')}
+
+
+
+ `;
+
+ 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);