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
This commit is contained in:
parent
16f3592e97
commit
84d0bc4ce4
15
asteroid.asd
15
asteroid.asd
|
|
@ -13,21 +13,22 @@
|
||||||
:radiance
|
:radiance
|
||||||
:i-log4cl
|
:i-log4cl
|
||||||
:r-clip
|
:r-clip
|
||||||
:cl-json
|
|
||||||
:dexador
|
|
||||||
:lass
|
:lass
|
||||||
:r-data-model
|
:cl-json
|
||||||
:cl-fad
|
:alexandria
|
||||||
:local-time
|
:local-time
|
||||||
:taglib
|
:taglib
|
||||||
(:interface :database)
|
|
||||||
:r-data-model
|
:r-data-model
|
||||||
|
:ironclad
|
||||||
|
:babel
|
||||||
|
:cl-fad
|
||||||
|
(:interface :database)
|
||||||
(:interface :user))
|
(:interface :user))
|
||||||
|
|
||||||
:pathname "./"
|
:pathname "./"
|
||||||
:components ((:file "app-utils")
|
:components ((:file "app-utils")
|
||||||
(:file "module")
|
(:file "module")
|
||||||
(:file "database")
|
(:file "database")
|
||||||
(:file "stream-media")
|
(:file "stream-media")
|
||||||
(:file "users")
|
(:file "user-management")
|
||||||
|
(:file "auth-routes")
|
||||||
(:file "asteroid")))
|
(:file "asteroid")))
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,30 @@
|
||||||
(defparameter *supported-formats* '("mp3" "flac" "ogg" "wav"))
|
(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
|
;; API Routes
|
||||||
(define-page admin-scan-library #@"/admin/scan-library" ()
|
(define-page admin-scan-library #@"/admin/scan-library" ()
|
||||||
"API endpoint to scan music library"
|
"API endpoint to scan music library"
|
||||||
|
(require-role :admin)
|
||||||
(handler-case
|
(handler-case
|
||||||
(let ((tracks-added (scan-music-library)))
|
(let ((tracks-added (scan-music-library)))
|
||||||
(setf (radiance:header "Content-Type") "application/json")
|
(setf (radiance:header "Content-Type") "application/json")
|
||||||
|
|
@ -39,6 +60,7 @@
|
||||||
|
|
||||||
(define-page admin-tracks #@"/admin/tracks" ()
|
(define-page admin-tracks #@"/admin/tracks" ()
|
||||||
"API endpoint to view all tracks in database"
|
"API endpoint to view all tracks in database"
|
||||||
|
(require-authentication)
|
||||||
(handler-case
|
(handler-case
|
||||||
(let ((tracks (db:select "tracks" (db:query :all))))
|
(let ((tracks (db:select "tracks" (db:query :all))))
|
||||||
(setf (radiance:header "Content-Type") "application/json")
|
(setf (radiance:header "Content-Type") "application/json")
|
||||||
|
|
@ -46,15 +68,13 @@
|
||||||
`(("status" . "success")
|
`(("status" . "success")
|
||||||
("tracks" . ,(mapcar (lambda (track)
|
("tracks" . ,(mapcar (lambda (track)
|
||||||
`(("id" . ,(gethash "_id" track))
|
`(("id" . ,(gethash "_id" track))
|
||||||
("title" . ,(gethash "title" track))
|
("title" . ,(first (gethash "title" track)))
|
||||||
("artist" . ,(gethash "artist" track))
|
("artist" . ,(first (gethash "artist" track)))
|
||||||
("album" . ,(gethash "album" track))
|
("album" . ,(first (gethash "album" track)))
|
||||||
("duration" . ,(gethash "duration" track))
|
("duration" . ,(first (gethash "duration" track)))
|
||||||
("file-path" . ,(gethash "file-path" track))
|
("format" . ,(first (gethash "format" track)))
|
||||||
("format" . ,(gethash "format" track))
|
("bitrate" . ,(first (gethash "bitrate" track)))
|
||||||
("bitrate" . ,(gethash "bitrate" track))
|
("play-count" . ,(first (gethash "play-count" track)))))
|
||||||
("added-date" . ,(gethash "added-date" track))
|
|
||||||
("play-count" . ,(gethash "play-count" track))))
|
|
||||||
tracks)))))
|
tracks)))))
|
||||||
(error (e)
|
(error (e)
|
||||||
(setf (radiance:header "Content-Type") "application/json")
|
(setf (radiance:header "Content-Type") "application/json")
|
||||||
|
|
@ -220,13 +240,9 @@
|
||||||
`(("status" . "success")
|
`(("status" . "success")
|
||||||
("player" . ,(get-player-status)))))
|
("player" . ,(get-player-status)))))
|
||||||
|
|
||||||
;; Configure static file serving for other files
|
;; Front page
|
||||||
(define-page static #@"/static/(.*)" (:uri-groups (path))
|
(define-page front-page #@"/" ()
|
||||||
(serve-file (merge-pathnames (concatenate 'string "static/" path)
|
"Main front page"
|
||||||
(asdf:system-source-directory :asteroid))))
|
|
||||||
|
|
||||||
;; RADIANCE route handlers
|
|
||||||
(define-page index #@"/" ()
|
|
||||||
(let ((template-path (merge-pathnames "template/front-page.chtml"
|
(let ((template-path (merge-pathnames "template/front-page.chtml"
|
||||||
(asdf:system-source-directory :asteroid))))
|
(asdf:system-source-directory :asteroid))))
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
|
|
@ -241,7 +257,15 @@
|
||||||
:now-playing-album "Startup Sounds"
|
:now-playing-album "Startup Sounds"
|
||||||
:now-playing-duration "∞")))
|
: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" ()
|
(define-page admin #@"/admin" ()
|
||||||
|
"Admin dashboard"
|
||||||
|
(require-authentication)
|
||||||
(let ((template-path (merge-pathnames "template/admin.chtml"
|
(let ((template-path (merge-pathnames "template/admin.chtml"
|
||||||
(asdf:system-source-directory :asteroid)))
|
(asdf:system-source-directory :asteroid)))
|
||||||
(track-count (handler-case
|
(track-count (handler-case
|
||||||
|
|
@ -249,7 +273,7 @@
|
||||||
(error () 0))))
|
(error () 0))))
|
||||||
(clip:process-to-string
|
(clip:process-to-string
|
||||||
(plump:parse (alexandria:read-file-into-string template-path))
|
(plump:parse (alexandria:read-file-into-string template-path))
|
||||||
:title "Asteroid Radio - Admin Dashboard"
|
:title "🎵 ASTEROID RADIO - Admin Dashboard"
|
||||||
:server-status "🟢 Running"
|
:server-status "🟢 Running"
|
||||||
:database-status (handler-case
|
:database-status (handler-case
|
||||||
(if (db:connected-p) "🟢 Connected" "🔴 Disconnected")
|
(if (db:connected-p) "🟢 Connected" "🔴 Disconnected")
|
||||||
|
|
@ -292,6 +316,11 @@
|
||||||
"Start the Asteroid Radio RADIANCE server"
|
"Start the Asteroid Radio RADIANCE server"
|
||||||
(format t "Starting Asteroid Radio RADIANCE server on port ~a~%" port)
|
(format t "Starting Asteroid Radio RADIANCE server on port ~a~%" port)
|
||||||
(compile-styles) ; Generate CSS file using LASS
|
(compile-styles) ; Generate CSS file using LASS
|
||||||
|
|
||||||
|
;; Ensure RADIANCE environment is properly set before startup
|
||||||
|
(unless (radiance:environment)
|
||||||
|
(setf (radiance:environment) "default"))
|
||||||
|
|
||||||
(radiance:startup)
|
(radiance:startup)
|
||||||
(format t "Server started! Visit http://localhost:~a/asteroid/~%" port))
|
(format t "Server started! Visit http://localhost:~a/asteroid/~%" port))
|
||||||
|
|
||||||
|
|
@ -312,6 +341,17 @@
|
||||||
(format t "~%Received interrupt, stopping server...~%")
|
(format t "~%Received interrupt, stopping server...~%")
|
||||||
(stop-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))
|
(defun -main (&optional args (debug t))
|
||||||
(declare (ignorable args))
|
(declare (ignorable args))
|
||||||
(format t "~&args of asteroid: ~A~%" args)
|
(format t "~&args of asteroid: ~A~%" args)
|
||||||
|
|
@ -319,5 +359,12 @@
|
||||||
(format t "Starting RADIANCE web server...~%")
|
(format t "Starting RADIANCE web server...~%")
|
||||||
(when debug
|
(when debug
|
||||||
(slynk:create-server :port 4009 :dont-close t))
|
(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))
|
(run-server))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Asteroid Radio - Login</title>
|
||||||
|
<link rel='stylesheet' href='/static/asteroid.css'>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class='container'>
|
||||||
|
<h1>🎵 ASTEROID RADIO - LOGIN</h1>
|
||||||
|
<div class='auth-container'>
|
||||||
|
<div class='auth-form'>
|
||||||
|
<h2>System Access</h2>
|
||||||
|
<div class='message error'>Invalid username or password</div>
|
||||||
|
<form method='post' action='/asteroid/login'>
|
||||||
|
<div class='form-group'>
|
||||||
|
<label>Username:</label>
|
||||||
|
<input type='text' name='username' required>
|
||||||
|
</div>
|
||||||
|
<div class='form-group'>
|
||||||
|
<label>Password:</label>
|
||||||
|
<input type='password' name='password' required>
|
||||||
|
</div>
|
||||||
|
<div class='form-actions'>
|
||||||
|
<button type='submit' class='btn btn-primary' style='width: 100%;'>LOGIN</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class='panel' style='margin-top: 20px; text-align: center;'>
|
||||||
|
<strong style='color: #ff6600;'>Default Admin Credentials:</strong><br>
|
||||||
|
Username: <code style='color: #00ff00;'>admin</code><br>
|
||||||
|
Password: <code style='color: #00ff00;'>asteroid123</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>"))
|
||||||
|
;; Show login form (no POST data)
|
||||||
|
"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Asteroid Radio - Login</title>
|
||||||
|
<link rel='stylesheet' href='/static/asteroid.css'>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class='container'>
|
||||||
|
<h1>🎵 ASTEROID RADIO - LOGIN</h1>
|
||||||
|
<div class='auth-container'>
|
||||||
|
<div class='auth-form'>
|
||||||
|
<h2>System Access</h2>
|
||||||
|
<form method='post' action='/asteroid/login'>
|
||||||
|
<div class='form-group'>
|
||||||
|
<label>Username:</label>
|
||||||
|
<input type='text' name='username' required>
|
||||||
|
</div>
|
||||||
|
<div class='form-group'>
|
||||||
|
<label>Password:</label>
|
||||||
|
<input type='password' name='password' required>
|
||||||
|
</div>
|
||||||
|
<div class='form-actions'>
|
||||||
|
<button type='submit' class='btn btn-primary' style='width: 100%;'>LOGIN</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div class='panel' style='margin-top: 20px; text-align: center;'>
|
||||||
|
<strong style='color: #ff6600;'>Default Admin Credentials:</strong><br>
|
||||||
|
Username: <code style='color: #00ff00;'>admin</code><br>
|
||||||
|
Password: <code style='color: #00ff00;'>asteroid123</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>")))
|
||||||
|
|
||||||
|
;; 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)))))))
|
||||||
|
|
@ -9,6 +9,10 @@
|
||||||
;; Load RADIANCE first, then handle environment
|
;; Load RADIANCE first, then handle environment
|
||||||
(ql:quickload :radiance)
|
(ql:quickload :radiance)
|
||||||
|
|
||||||
|
;; Ensure RADIANCE environment is set before loading
|
||||||
|
(unless (radiance:environment)
|
||||||
|
(setf (radiance:environment) "default"))
|
||||||
|
|
||||||
;; Load the system with RADIANCE environment handling
|
;; Load the system with RADIANCE environment handling
|
||||||
(handler-bind ((radiance-core:environment-not-set
|
(handler-bind ((radiance-core:environment-not-set
|
||||||
(lambda (c)
|
(lambda (c)
|
||||||
|
|
|
||||||
|
|
@ -22,5 +22,14 @@
|
||||||
(created-date :integer)
|
(created-date :integer)
|
||||||
(track-ids :text))))
|
(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~%"))
|
(format t "Database collections initialized~%"))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -486,6 +486,294 @@ body input{
|
||||||
font-family: Courier New, monospace;
|
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;
|
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;
|
||||||
}
|
}
|
||||||
|
|
@ -384,6 +384,218 @@
|
||||||
:border-radius "3px"
|
:border-radius "3px"
|
||||||
:font-family "Courier New, monospace")
|
: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
|
;; Center alignment for player page
|
||||||
(body.player-page
|
(body.player-page
|
||||||
:text-align center))
|
:text-align center))
|
||||||
|
|
|
||||||
|
|
@ -91,17 +91,44 @@
|
||||||
<!-- Player Control -->
|
<!-- Player Control -->
|
||||||
<div class="admin-section">
|
<div class="admin-section">
|
||||||
<h2>Player Control</h2>
|
<h2>Player Control</h2>
|
||||||
<div class="player-controls">
|
<div class="card">
|
||||||
<button id="player-play" class="btn btn-success">▶️ Play</button>
|
<h3>🎵 Player Control</h3>
|
||||||
<button id="player-pause" class="btn btn-warning">⏸️ Pause</button>
|
<div class="player-controls">
|
||||||
<button id="player-stop" class="btn btn-danger">⏹️ Stop</button>
|
<button class="btn btn-primary" onclick="playTrack()">▶️ Play</button>
|
||||||
<button id="player-resume" class="btn btn-info">▶️ Resume</button>
|
<button class="btn btn-secondary" onclick="pausePlayer()">⏸️ Pause</button>
|
||||||
|
<button class="btn btn-secondary" onclick="stopPlayer()">⏹️ Stop</button>
|
||||||
|
<button class="btn btn-secondary" onclick="resumePlayer()">▶️ Resume</button>
|
||||||
|
</div>
|
||||||
|
<div id="player-status" class="status-info">
|
||||||
|
Status: <span id="status-text">Unknown</span><br>
|
||||||
|
Current Track: <span id="current-track">None</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="player-status">
|
<div class="card">
|
||||||
<p>Status: <span id="player-state">Stopped</span></p>
|
<h3>👥 User Management</h3>
|
||||||
<p>Current Track: <span id="current-track">None</span></p>
|
<div class="user-stats" id="user-stats">
|
||||||
<p>Position: <span id="current-position">0</span>s</p>
|
<div class="stat-card">
|
||||||
|
<span class="stat-number" id="total-users">0</span>
|
||||||
|
<span class="stat-label">Total Users</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-number" id="active-users">0</span>
|
||||||
|
<span class="stat-label">Active Users</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-number" id="admin-users">0</span>
|
||||||
|
<span class="stat-label">Admins</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<span class="stat-number" id="dj-users">0</span>
|
||||||
|
<span class="stat-label">DJs</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<button class="btn btn-primary" onclick="loadUsers()">👥 Manage Users</button>
|
||||||
|
<button class="btn btn-secondary" onclick="showCreateUser()">➕ Create User</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -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.');
|
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 = `
|
||||||
|
<h3>User Management</h3>
|
||||||
|
<table class="users-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Username</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Last Login</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${users.map(user => `
|
||||||
|
<tr>
|
||||||
|
<td>${user.username}</td>
|
||||||
|
<td>${user.email}</td>
|
||||||
|
<td>
|
||||||
|
<select onchange="updateUserRole('${user.id}', this.value)">
|
||||||
|
<option value="listener" ${user.role === 'listener' ? 'selected' : ''}>Listener</option>
|
||||||
|
<option value="dj" ${user.role === 'dj' ? 'selected' : ''}>DJ</option>
|
||||||
|
<option value="admin" ${user.role === 'admin' ? 'selected' : ''}>Admin</option>
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>${user.active ? '✅ Active' : '❌ Inactive'}</td>
|
||||||
|
<td>${user['last-login'] ? new Date(user['last-login'] * 1000).toLocaleString() : 'Never'}</td>
|
||||||
|
<td class="user-actions">
|
||||||
|
${user.active ?
|
||||||
|
`<button class="btn btn-danger" onclick="deactivateUser('${user.id}')">Deactivate</button>` :
|
||||||
|
`<button class="btn btn-success" onclick="activateUser('${user.id}')">Activate</button>`
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button class="btn btn-secondary" onclick="hideUsersTable()">Close</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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
|
// Update player status every 5 seconds
|
||||||
setInterval(updatePlayerStatus, 5000);
|
setInterval(updatePlayerStatus, 5000);
|
||||||
|
|
||||||
|
// Update user stats every 30 seconds
|
||||||
|
setInterval(loadUserStats, 30000);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -4,41 +4,49 @@
|
||||||
<title data-text="title">🎵 ASTEROID RADIO 🎵</title>
|
<title data-text="title">🎵 ASTEROID RADIO 🎵</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" type="text/css" href="/static/asteroid.css">
|
<link rel="stylesheet" type="text/css" href="/asteroid/static/asteroid.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 data-text="station-name">🎵 ASTEROID RADIO 🎵</h1>
|
<header>
|
||||||
<div class="status">
|
<h1 data-text="station-name">🎵 ASTEROID RADIO 🎵</h1>
|
||||||
<h2>Station Status</h2>
|
<nav>
|
||||||
<p data-text="status-message">🟢 LIVE - Broadcasting asteroid music for hackers</p>
|
<a href="/asteroid/">Home</a>
|
||||||
<p>Current listeners: <span data-text="listeners">0</span></p>
|
<a href="/asteroid/player">Player</a>
|
||||||
<p>Stream quality: <span data-text="stream-quality">128kbps MP3</span></p>
|
<a href="/asteroid/admin">Admin</a>
|
||||||
</div>
|
<a href="/asteroid/status">Status</a>
|
||||||
<div class="nav">
|
<a href="/asteroid/login">Login</a>
|
||||||
<a href="/admin">Admin Dashboard</a>
|
<a href="/asteroid/register">Register</a>
|
||||||
<a href="/player/">Web Player</a>
|
</nav>
|
||||||
<a href="http://localhost:8000/asteroid.mp3" target="_blank">🎵 Live Stream</a>
|
</header>
|
||||||
<a href="/status">API Status</a>
|
|
||||||
</div>
|
<main>
|
||||||
|
<div class="status">
|
||||||
<div class="live-stream">
|
<h2>Station Status</h2>
|
||||||
<h2>🔴 LIVE STREAM</h2>
|
<p data-text="status-message">🟢 LIVE - Broadcasting asteroid music for hackers</p>
|
||||||
<p><strong>Stream URL:</strong> <code>http://localhost:8000/asteroid.mp3</code></p>
|
<p>Current listeners: <span data-text="listeners">0</span></p>
|
||||||
<p><strong>Format:</strong> MP3 128kbps Stereo</p>
|
<p>Stream quality: <span data-text="stream-quality">128kbps MP3</span></p>
|
||||||
<p><strong>Status:</strong> <span style="color: #00ff00;">● BROADCASTING</span></p>
|
</div>
|
||||||
<audio controls style="width: 100%; margin: 10px 0;">
|
|
||||||
<source src="http://localhost:8000/asteroid.mp3" type="audio/mpeg">
|
<div class="live-stream">
|
||||||
Your browser does not support the audio element.
|
<h2>🔴 LIVE STREAM</h2>
|
||||||
</audio>
|
<p><strong>Stream URL:</strong> <code>http://localhost:8000/asteroid.mp3</code></p>
|
||||||
</div>
|
<p><strong>Format:</strong> MP3 128kbps Stereo</p>
|
||||||
<div>
|
<p><strong>Status:</strong> <span style="color: #00ff00;">● BROADCASTING</span></p>
|
||||||
<h2>Now Playing</h2>
|
<audio controls style="width: 100%; margin: 10px 0;">
|
||||||
<p>Artist: <span data-text="now-playing-artist">The Void</span></p>
|
<source src="http://localhost:8000/asteroid.mp3" type="audio/mpeg">
|
||||||
<p>Track: <span data-text="now-playing-track">Silence</span></p>
|
Your browser does not support the audio element.
|
||||||
<p>Album: <span data-text="now-playing-album">Startup Sounds</span></p>
|
</audio>
|
||||||
<p>Duration: <span data-text="now-playing-duration">∞</span></p>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="now-playing">
|
||||||
|
<h2>Now Playing</h2>
|
||||||
|
<p>Artist: <span data-text="now-playing-artist">The Void</span></p>
|
||||||
|
<p>Track: <span data-text="now-playing-track">Silence</span></p>
|
||||||
|
<p>Album: <span data-text="now-playing-album">Startup Sounds</span></p>
|
||||||
|
<p>Duration: <span data-text="now-playing-duration">∞</span></p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -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"))))
|
||||||
Loading…
Reference in New Issue