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
This commit is contained in:
Glenn Thompson 2025-09-16 21:45:58 +03:00 committed by Brian O'Reilly
parent 84d0bc4ce4
commit b1a61fae00
3 changed files with 92 additions and 89 deletions

View File

@ -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
"<!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>"))
(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)
"<!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>")))
(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

40
template/login.chtml Normal file
View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title data-text="title">Asteroid Radio - Login</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" 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" data-attr="style" data-attr-value="display-error">
<span data-text="error-message">Invalid username or password</span>
</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>

View File

@ -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"