diff --git a/asteroid.asd b/asteroid.asd index 7a4d6ac..a42e30a 100644 --- a/asteroid.asd +++ b/asteroid.asd @@ -45,7 +45,8 @@ (:module :parenscript :components ((:file "auth-ui") (:file "front-page") - (:file "profile"))) + (:file "profile") + (:file "users"))) (:file "stream-media") (:file "user-management") (:file "playlist-management") diff --git a/asteroid.lisp b/asteroid.lisp index 5ef76e7..7cf4c4a 100644 --- a/asteroid.lisp +++ b/asteroid.lisp @@ -511,6 +511,18 @@ (format t "ERROR generating profile.js: ~a~%" e) (format nil "// Error generating JavaScript: ~a~%" e)))) + ;; Serve ParenScript-compiled users.js + ((string= path "js/users.js") + (format t "~%=== SERVING PARENSCRIPT users.js ===~%") + (setf (content-type *response*) "application/javascript") + (handler-case + (let ((js (generate-users-js))) + (format t "DEBUG: Generated JS length: ~a~%" (if js (length js) "NIL")) + (if js js "// Error: No JavaScript generated")) + (error (e) + (format t "ERROR generating users.js: ~a~%" e) + (format nil "// Error generating JavaScript: ~a~%" e)))) + ;; Serve regular static file (t (serve-file (merge-pathnames (format nil "static/~a" path) diff --git a/parenscript/users.lisp b/parenscript/users.lisp new file mode 100644 index 0000000..504319f --- /dev/null +++ b/parenscript/users.lisp @@ -0,0 +1,203 @@ +;;;; users.lisp - ParenScript version of users.js +;;;; User management page for admins + +(in-package #:asteroid) + +(defparameter *users-js* + (ps:ps* + '(progn + + ;; Load user stats + (defun load-user-stats () + (ps:chain + (fetch "/api/asteroid/user-stats") + (then (lambda (response) (ps:chain response (json)))) + (then (lambda (result) + (let ((data (or (ps:@ result data) result))) + (when (and (= (ps:@ data status) "success") (ps:@ data stats)) + (let ((stats (ps:@ data stats))) + (setf (ps:@ (ps:chain document (get-element-by-id "total-users")) text-content) + (or (ps:getprop stats "total-users") 0)) + (setf (ps:@ (ps:chain document (get-element-by-id "active-users")) text-content) + (or (ps:getprop stats "active-users") 0)) + (setf (ps:@ (ps:chain document (get-element-by-id "admin-users")) text-content) + (or (ps:getprop stats "admins") 0)) + (setf (ps:@ (ps:chain document (get-element-by-id "dj-users")) text-content) + (or (ps:getprop stats "djs") 0))))))) + (catch (lambda (error) + (ps:chain console (error "Error loading user stats:" error)))))) + + ;; Load users list + (defun load-users () + (ps:chain + (fetch "/api/asteroid/users") + (then (lambda (response) (ps:chain response (json)))) + (then (lambda (result) + (let ((data (or (ps:@ result data) result))) + (when (= (ps:@ data status) "success") + (show-users-table (ps:@ data users)) + (setf (ps:@ (ps:chain document (get-element-by-id "users-list-section")) style display) "block"))))) + (catch (lambda (error) + (ps:chain console (error "Error loading users:" error)) + (alert "Error loading users. Please try again."))))) + + ;; Show users table + (defun show-users-table (users) + (let ((container (ps:chain document (get-element-by-id "users-container"))) + (users-html (ps:chain users + (map (lambda (user) + (+ "" + "" (ps:@ user username) "" + "" (ps:@ user email) "" + "" + "" + "" + "" (if (ps:@ user active) "✅ Active" "❌ Inactive") "" + "" (if (ps:getprop user "last-login") + (ps:chain (ps:new (-date (* (ps:getprop user "last-login") 1000))) (to-locale-string)) + "Never") "" + "" + (if (ps:@ user active) + (+ "") + (+ "")) + "" + ""))) + (join "")))) + (setf (ps:@ container inner-h-t-m-l) + (+ "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + users-html + "" + "
UsernameEmailRoleStatusLast LoginActions
" + "")))) + + (defun hide-users-table () + (setf (ps:@ (ps:chain document (get-element-by-id "users-list-section")) style display) "none")) + + ;; Update user role + (defun update-user-role (user-id new-role) + (let ((form-data (ps:new (-form-data)))) + (ps:chain form-data (append "role" new-role)) + + (ps:chain + (fetch (+ "/api/asteroid/users/" user-id "/role") + (ps:create :method "POST" :body form-data)) + (then (lambda (response) (ps:chain response (json)))) + (then (lambda (result) + (if (= (ps:@ result status) "success") + (progn + (load-user-stats) + (alert "User role updated successfully")) + (alert (+ "Error updating user role: " (ps:@ result message)))))) + (catch (lambda (error) + (ps:chain console (error "Error updating user role:" error)) + (alert "Error updating user role. Please try again.")))))) + + ;; Deactivate user + (defun deactivate-user (user-id) + (when (not (confirm "Are you sure you want to deactivate this user?")) + (return)) + + (ps:chain + (fetch (+ "/api/asteroid/users/" user-id "/deactivate") + (ps:create :method "POST")) + (then (lambda (response) (ps:chain response (json)))) + (then (lambda (result) + (if (= (ps:@ result status) "success") + (progn + (load-users) + (load-user-stats) + (alert "User deactivated successfully")) + (alert (+ "Error deactivating user: " (ps:@ result message)))))) + (catch (lambda (error) + (ps:chain console (error "Error deactivating user:" error)) + (alert "Error deactivating user. Please try again."))))) + + ;; Activate user + (defun activate-user (user-id) + (ps:chain + (fetch (+ "/api/asteroid/users/" user-id "/activate") + (ps:create :method "POST")) + (then (lambda (response) (ps:chain response (json)))) + (then (lambda (result) + (if (= (ps:@ result status) "success") + (progn + (load-users) + (load-user-stats) + (alert "User activated successfully")) + (alert (+ "Error activating user: " (ps:@ result message)))))) + (catch (lambda (error) + (ps:chain console (error "Error activating user:" error)) + (alert "Error activating user. Please try again."))))) + + ;; Toggle create user form + (defun toggle-create-user-form () + (let ((form (ps:chain document (get-element-by-id "create-user-form")))) + (if (= (ps:@ form style display) "none") + (progn + (setf (ps:@ form style display) "block") + (setf (ps:@ (ps:chain document (get-element-by-id "new-username")) value) "") + (setf (ps:@ (ps:chain document (get-element-by-id "new-email")) value) "") + (setf (ps:@ (ps:chain document (get-element-by-id "new-password")) value) "") + (setf (ps:@ (ps:chain document (get-element-by-id "new-role")) value) "listener")) + (setf (ps:@ form style display) "none")))) + + ;; Create new user + (defun create-new-user (event) + (ps:chain event (prevent-default)) + + (let ((username (ps:@ (ps:chain document (get-element-by-id "new-username")) value)) + (email (ps:@ (ps:chain document (get-element-by-id "new-email")) value)) + (password (ps:@ (ps:chain document (get-element-by-id "new-password")) value)) + (role (ps:@ (ps:chain document (get-element-by-id "new-role")) value)) + (form-data (ps:new (-form-data)))) + + (ps:chain form-data (append "username" username)) + (ps:chain form-data (append "email" email)) + (ps:chain form-data (append "password" password)) + (ps:chain form-data (append "role" role)) + + (ps:chain + (fetch "/api/asteroid/users/create" + (ps:create :method "POST" :body form-data)) + (then (lambda (response) (ps:chain response (json)))) + (then (lambda (result) + (let ((data (or (ps:@ result data) result))) + (if (= (ps:@ data status) "success") + (progn + (alert (+ "User \"" username "\" created successfully!")) + (toggle-create-user-form) + (load-user-stats) + (load-users)) + (alert (+ "Error creating user: " (or (ps:@ data message) (ps:@ result message)))))))) + (catch (lambda (error) + (ps:chain console (error "Error creating user:" error)) + (alert "Error creating user. Please try again.")))))) + + ;; Initialize on page load + (ps:chain document + (add-event-listener + "DOMContentLoaded" + load-user-stats)) + + ;; Update user stats every 30 seconds + (set-interval load-user-stats 30000))) + "Compiled JavaScript for users management - generated at load time") + +(defun generate-users-js () + "Return the pre-compiled JavaScript for users page" + *users-js*) diff --git a/static/js/users.js b/static/js/users.js.original similarity index 100% rename from static/js/users.js rename to static/js/users.js.original