Compare commits

...

13 Commits

Author SHA1 Message Date
Brian O'Reilly ffd178c555 TODO updates, Nov 5, 2025 2025-11-05 17:48:05 -05:00
Brian O'Reilly 270c0ad679 update TODO.org 2025-11-04 17:43:03 -05:00
Brian O'Reilly 1665efe1e1 refactor: Implement Lispy improvements - templates, strings, and error handling
This commit implements three major refactorings to make the codebase more
idiomatic and maintainable:

1. Template Path Centralization
   - Add *template-directory* parameter and helper functions
   - Replace 11+ instances of repetitive template loading boilerplate
   - New functions: template-path, load-template in template-utils.lisp

2. String Construction with FORMAT
   - Replace concatenate with format for external URLs (Icecast, static files)
   - Maintain Radiance URI handling for internal routes
   - Applied to stream URLs, status endpoints, and API responses

3. Error Handling with Custom Conditions
   - NEW FILE: conditions.lisp with comprehensive error hierarchy
   - Custom conditions: not-found-error, authentication-error,
     authorization-error, validation-error, database-error, asteroid-stream-error
   - Helper macros: with-error-handling, with-db-error-handling
   - Helper functions: signal-not-found, signal-validation-error, etc.
   - Refactored 19 API endpoints and page routes
   - Proper HTTP status codes: 404, 401, 403, 400, 500

Changes:
- conditions.lisp: NEW (180+ lines of error handling infrastructure)
- asteroid.asd: Add conditions.lisp to system components
- asteroid.lisp: Refactor 30+ endpoints, eliminate 200+ lines of boilerplate
- template-utils.lisp: Add centralized template loading helpers
- frontend-partials.lisp: Update template loading and string construction

Net result: -97 lines of code, significantly improved error handling,
more maintainable and idiomatic Common Lisp.

All changes tested and verified:
- Clean build
- All endpoints functional
- Error handling returns proper HTTP codes
- No regressions
2025-11-04 17:42:41 -05:00
Brian O'Reilly 40a49c1c25 many little changes all in a line.
my feature branch touches too many things. fix merge conflicts in
rebase to main.
2025-11-04 17:42:41 -05:00
Brian O'Reilly 30b2d88f6a small moves, ellie. small moves. 2025-11-04 17:42:41 -05:00
Brian O'Reilly 3dd9c2d469 many state needs construction at build time. 2025-11-04 17:42:41 -05:00
Brian O'Reilly b9b3feda6b refactor: Implement Lispy improvements - templates, strings, and error handling
This commit implements three major refactorings to make the codebase more
idiomatic and maintainable:

1. Template Path Centralization
   - Add *template-directory* parameter and helper functions
   - Replace 11+ instances of repetitive template loading boilerplate
   - New functions: template-path, load-template in template-utils.lisp

2. String Construction with FORMAT
   - Replace concatenate with format for external URLs (Icecast, static files)
   - Maintain Radiance URI handling for internal routes
   - Applied to stream URLs, status endpoints, and API responses

3. Error Handling with Custom Conditions
   - NEW FILE: conditions.lisp with comprehensive error hierarchy
   - Custom conditions: not-found-error, authentication-error,
     authorization-error, validation-error, database-error, asteroid-stream-error
   - Helper macros: with-error-handling, with-db-error-handling
   - Helper functions: signal-not-found, signal-validation-error, etc.
   - Refactored 19 API endpoints and page routes
   - Proper HTTP status codes: 404, 401, 403, 400, 500

Changes:
- conditions.lisp: NEW (180+ lines of error handling infrastructure)
- asteroid.asd: Add conditions.lisp to system components
- asteroid.lisp: Refactor 30+ endpoints, eliminate 200+ lines of boilerplate
- template-utils.lisp: Add centralized template loading helpers
- frontend-partials.lisp: Update template loading and string construction

Net result: -97 lines of code, significantly improved error handling,
more maintainable and idiomatic Common Lisp.

All changes tested and verified:
- Clean build
- All endpoints functional
- Error handling returns proper HTTP codes
- No regressions
2025-11-04 17:42:41 -05:00
Brian O'Reilly 69b0b2ca9e it really is best not to rewrite history. bring this back from the dead. 2025-11-04 17:42:41 -05:00
Brian O'Reilly b80dea5a08 many little changes all in a line.
my feature branch touches too many things. fix merge conflicts in
rebase to main.
2025-11-04 17:42:41 -05:00
Brian O'Reilly 5882141cfa refactor: Implement Lispy improvements - templates, strings, and error handling
This commit implements three major refactorings to make the codebase more
idiomatic and maintainable:

1. Template Path Centralization
   - Add *template-directory* parameter and helper functions
   - Replace 11+ instances of repetitive template loading boilerplate
   - New functions: template-path, load-template in template-utils.lisp

2. String Construction with FORMAT
   - Replace concatenate with format for external URLs (Icecast, static files)
   - Maintain Radiance URI handling for internal routes
   - Applied to stream URLs, status endpoints, and API responses

3. Error Handling with Custom Conditions
   - NEW FILE: conditions.lisp with comprehensive error hierarchy
   - Custom conditions: not-found-error, authentication-error,
     authorization-error, validation-error, database-error, asteroid-stream-error
   - Helper macros: with-error-handling, with-db-error-handling
   - Helper functions: signal-not-found, signal-validation-error, etc.
   - Refactored 19 API endpoints and page routes
   - Proper HTTP status codes: 404, 401, 403, 400, 500

Changes:
- conditions.lisp: NEW (180+ lines of error handling infrastructure)
- asteroid.asd: Add conditions.lisp to system components
- asteroid.lisp: Refactor 30+ endpoints, eliminate 200+ lines of boilerplate
- template-utils.lisp: Add centralized template loading helpers
- frontend-partials.lisp: Update template loading and string construction

Net result: -97 lines of code, significantly improved error handling,
more maintainable and idiomatic Common Lisp.

All changes tested and verified:
- Clean build
- All endpoints functional
- Error handling returns proper HTTP codes
- No regressions
2025-11-04 17:42:06 -05:00
Brian O'Reilly 85881b8fb6 small moves, ellie. small moves. 2025-11-04 17:37:13 -05:00
glenneth 4c99ded7f0 feat: Add password management and fix listener count
- Add user password change functionality in profile page
- Add admin password reset functionality in admin dashboard
- Fix listener count to show total from Icecast root tag
- Replace concatenate with format
- Add with-error-handling to API endpoints
- Make Icecast port binding configurable via ICECAST_BIND env var
- Add comprehensive docstrings to public functions

Note: Password reset has known issue with LambdaLite db:update
not updating password-hash field. Issue reported as an issue.
2025-11-04 16:26:29 -05:00
glenneth c58c8a255c security: Remove hardcoded admin credentials from login page
Removed the display of default admin username and password from the
login page template. This information should not be publicly visible
as it poses a security risk in production environments.

Administrators should use secure credential management practices
and change default passwords during initial setup.
2025-11-03 22:40:24 -05:00
18 changed files with 311 additions and 64 deletions

View File

@ -6,7 +6,7 @@ ENTRY=-main
.PHONY: $(OUT) .PHONY: $(OUT)
$(OUT): clean $(OUT): clean
sbcl --load build-executable.lisp sbcl --load build-asteroid.lisp
quicklisp-manifest.txt: *.asd quicklisp-manifest.txt: *.asd
sbcl --non-interactive \ sbcl --non-interactive \

View File

@ -9,7 +9,7 @@
- [X] Set up DNS - [X] Set up DNS
- [X] Create user accounts - [X] Create user accounts
* Deploy the system * Deploy the system [10/10]
- [X] Install and configure HAproxy - [X] Install and configure HAproxy
- [X] Create a user to contain asteroid - [X] Create a user to contain asteroid
- [X] Checkout asteroid in ~asteroid on b612 - [X] Checkout asteroid in ~asteroid on b612
@ -21,21 +21,27 @@
- [X] Start asteroid, check the stream (Underworld:Juanita/Kiteless) - [X] Start asteroid, check the stream (Underworld:Juanita/Kiteless)
- [X] Announce that the system is live in #asteroid.radio - [X] Announce that the system is live in #asteroid.radio
** PROBLEMS ** PROBLEMS [3/12]
1) Liquidsoap is exposing its management console via telnet on the exterior network interface of b612.asteroid.radio 1) [X] Liquidsoap is exposing its management console via telnet on the exterior network interface of b612.asteroid.radio
2) icecast is also binding the external interface on b612, which it 2) [X] icecast is also binding the external interface on b612, which it
should not be. HAproxy is there to mediate this flow. should not be. HAproxy is there to mediate this flow.
3) We're still on the built in i-lambdalite database 3) [ ] We're still on the built in i-lambdalite database
4) The templates still advertise the default administrator password, 4) [X] The templates still advertise the default administrator password,
which is no bueno. which is no bueno.
5) We need to work out the TLS situation with letsencrypt, and 5) [ ] We need to work out the TLS situation with letsencrypt, and
integrate it into HAproxy. integrate it into HAproxy.
6) the administrative interface should be beefed up.
5.1) Deactivate users 6) [ ] The administrative interface should be beefed up.
5.2) Change user access permissions 6.1) [ ] Deactivate users
7) User profile pages should probably be fleshed out. 6.2) [ ] Change user access permissions
8) the stream management features aren't there for Admins or DJs. 6.3) [ ] Listener statistics, breakdown by day/hour, new users, % changed &c
9) The "Scan Library" feature is not working in the main branch
7) [ ] When the player is paused, there are pretty serious stream sync issues in the form of stuttering for some time after the stream is unpaused.
8) [ ] User profile pages should probably be fleshed out.
9) [ ] the stream management features aren't there for Admins or DJs.
10) [ ] The "Scan Library" feature is not working in the main branch
11) [ ] The player widget should be styled so it fits the site theme on systems running 'light' thmes.
12) [ ] ensure each info field 'Listeners: ..' &c has only one instance per page.
* Server runtime configuration [0/1] * Server runtime configuration [0/1]
- [ ] parameterize all configuration for runtime loading [0/2] - [ ] parameterize all configuration for runtime loading [0/2]

View File

@ -33,6 +33,8 @@
:pathname "./" :pathname "./"
:components ((:file "app-utils") :components ((:file "app-utils")
(:file "module") (:file "module")
(:module :config
:components ((:file radiance-postgres)))
(:file "conditions") (:file "conditions")
(:file "database") (:file "database")
(:file "template-utils") (:file "template-utils")

View File

@ -8,9 +8,9 @@
(in-package :asteroid) (in-package :asteroid)
;; Define as RADIANCE module ;; Define as RADIANCE module
(define-module asteroid ;; (define-module asteroid
(:use #:cl #:radiance #:lass #:r-clip) ;; (:use #:cl #:radiance #:lass #:r-clip)
(:domain "asteroid")) ;; (:domain "asteroid"))
;; Configuration -- this will be refactored to a dedicated ;; Configuration -- this will be refactored to a dedicated
;; configuration logic. Probably using 'ubiquity ;; configuration logic. Probably using 'ubiquity

View File

@ -105,3 +105,53 @@
(api-output `(("status" . "error") (api-output `(("status" . "error")
("message" . ,(format nil "Error creating user: ~a" e))) ("message" . ,(format nil "Error creating user: ~a" e)))
:status 500)))) :status 500))))
;; API: Change user's own password
(define-api asteroid/user/change-password (current-password new-password) ()
"API endpoint for users to change their own password"
(with-error-handling
(unless (and current-password new-password)
(error 'validation-error :message "Missing required fields"))
(unless (>= (length new-password) 8)
(error 'validation-error :message "New password must be at least 8 characters"))
(let* ((user-id (session-field 'user-id))
(username (when user-id
(let ((user (find-user-by-id user-id)))
(when user (gethash "username" user))))))
(unless username
(error 'authentication-error :message "Not authenticated"))
;; Verify current password
(unless (verify-user-credentials username current-password)
(error 'authentication-error :message "Current password is incorrect"))
;; Update password
(unless (reset-user-password username new-password)
(error 'database-error :message "Failed to update password"))
(api-output `(("status" . "success")
("message" . "Password changed successfully"))))))
;; API: Reset user password (admin only)
(define-api asteroid/admin/reset-password (username new-password) ()
"API endpoint for admins to reset any user's password"
(require-role :admin)
(with-error-handling
(unless (and username new-password)
(error 'validation-error :message "Missing required fields"))
(unless (>= (length new-password) 8)
(error 'validation-error :message "New password must be at least 8 characters"))
(let ((user (find-user-by-username username)))
(unless user
(error 'not-found-error :message (format nil "User not found: ~a" username)))
(unless (reset-user-password username new-password)
(error 'database-error :message "Failed to reset password"))
(api-output `(("status" . "success")
("message" . ,(format nil "Password reset for user: ~a" username)))))))

View File

@ -1,5 +1,15 @@
;; -*-lisp-*- ;; -*-lisp-*-
(unless *load-pathname*
(error "Please LOAD this file."))
(when (find-package :quicklisp)
(error "Please run this file as a script or from the Makefile."))
(defpackage #:asteroid-bootstrap
(:use #:cl)
(:export #:*root* #:path))
;; we require quicklisp to load our transitive dependencies. ;; we require quicklisp to load our transitive dependencies.
(load "~/quicklisp/setup.lisp") (load "~/quicklisp/setup.lisp")

View File

@ -7,6 +7,7 @@
(setf (config :database :connection) (setf (config :database :connection)
'(:type :postgres '(:type :postgres
:host "localhost" ; Change to "asteroid-postgres" when running in Docker :host "localhost" ; Change to "asteroid-postgres" when running in Docker
;; :host "asteroid-postgres"
:port 5432 :port 5432
:database "asteroid" :database "asteroid"
:username "asteroid" :username "asteroid"

View File

@ -30,6 +30,20 @@
(active :integer) (active :integer)
(created-date :integer) (created-date :integer)
(last-login :integer)))) (last-login :integer))))
(format t "Database collections initialized~%")) (unless (db:collection-exists-p "playlist_tracks")
(db:create "playlist_tracks" '((track_id :integer)
(position :ingeger)
(added_date :timestamp))))
;; TODO: the radiance db interface is too basic to contain anything
;; but strings, integers, booleans, and maybe timestamps... we will
;; need to rethink this. currently track/playlist relationships are
;; defined in the SQL file 'init-db.sql' referenced in the docker
;; config for postgresql, but our lisp code doesn't leverage it.
;; (unless (db:collection-exists-p "sessions")
;; (db:create "sessions" '(())))
(l:info "~2&Database collections initialized~%"))

View File

@ -3,7 +3,7 @@ services:
image: infiniteproject/icecast:latest image: infiniteproject/icecast:latest
container_name: asteroid-icecast container_name: asteroid-icecast
ports: ports:
- "127.0.0.1:8000:8000" - "${ICECAST_BIND:-127.0.0.1}:8000:8000"
volumes: volumes:
- ./icecast.xml:/etc/icecast.xml - ./icecast.xml:/etc/icecast.xml
environment: environment:

View File

@ -1,20 +1,20 @@
; meta (:version 1.0 :package "RADIANCE-CORE") ; meta (:version 1.0 :package "RADIANCE-CORE")
((:interfaces (:admin . "r-simple-admin") (:auth . "r-simple-auth") ((:interfaces (:admin . "r-simple-admin") (:auth . "r-simple-auth")
(:ban . "r-simple-ban") (:cache . "r-simple-cache") (:ban . "r-simple-ban") (:cache . "r-simple-cache")
(:data-model . "r-simple-model") (:database . "i-lambdalite") (:data-model . "r-simple-model") (:database . "i-lambdalite")
(:relational-database . "i-sqlite") (:logger . "i-verbose") (:relational-database . "i-sqlite") (:logger . "i-verbose")
(:mail . "i-smtp") (:profile . "r-simple-profile") (:rate . "r-simple-rate") (:mail . "i-smtp") (:profile . "r-simple-profile") (:rate . "r-simple-rate")
(:server . "i-hunchentoot") (:session . "r-simple-sessions") (:server . "i-hunchentoot") (:session . "r-simple-sessions")
(:user . "r-simple-users")) (:user . "r-simple-users"))
(:versions (:versions
. [hash-table equal ("radiance-core" :|2.2.0|) ("i-hunchentoot" :|1.1.0|) . [hash-table equal ("radiance-core" :|2.2.0|) ("i-hunchentoot" :|1.1.0|)
("asteroid" :|0.0.0|) ("i-log4cl" :|1.0.0|) ("r-clip" :|1.0.0|) ("asteroid" :|0.0.0|) ("i-log4cl" :|1.0.0|) ("r-clip" :|1.0.0|)
("r-data-model" :|1.0.1|) ("i-lambdalite" :|1.0.0|) ("r-data-model" :|1.0.1|) ("i-lambdalite" :|1.0.0|)
("r-simple-users" :|1.0.1|) ("r-simple-users" :|1.0.1|)
("r-simple-errors" :|1.0.0|) ("i-verbose" :|1.0.0|) ("r-simple-errors" :|1.0.0|) ("i-verbose" :|1.0.0|)
("r-simple-auth" :|1.0.0|) ("r-simple-sessions" :|1.0.1|) ("r-simple-auth" :|1.0.0|) ("r-simple-sessions" :|1.0.1|)
("r-ratify" :|1.0.0|) ("r-simple-rate" :|1.0.0|) ("r-ratify" :|1.0.0|) ("r-simple-rate" :|1.0.0|)
("r-simple-profile" :|1.0.0|)]) ("r-simple-profile" :|1.0.0|)])
(:domains "radiance" "localhost") (:domains "asteroid" "radiance" "asteroid.radio" "localhost")
(:startup :r-simple-errors :r-simple-sessions) (:routes) (:startup :r-simple-errors :r-simple-sessions) (:routes)
(:debugger . :if-swank-connected)) (:debugger . :if-swank-connected))

View File

@ -1,4 +1,4 @@
(in-package :asteroid) ^(in-package :asteroid)
(defun icecast-now-playing (icecast-base-url) (defun icecast-now-playing (icecast-base-url)
(let* ((icecast-url (format nil "~a/admin/stats.xml" icecast-base-url)) (let* ((icecast-url (format nil "~a/admin/stats.xml" icecast-base-url))
@ -46,15 +46,11 @@
(clip:process-to-string (clip:process-to-string
(load-template "partial/now-playing") (load-template "partial/now-playing")
:connection-error t :connection-error t
:stats nil)))) :stats nil))))))
(error (e)
(api-output `(("status" . "error")
("message" . ,(format nil "Error loading profile: ~a" e)))
:status 500))))
(define-api asteroid/partial/now-playing-inline () () (define-api asteroid/partial/now-playing-inline () ()
"Get inline text with now playing info (for admin dashboard and widgets)" "Get inline text with now playing info (for admin dashboard and widgets)"
(handler-case (with-error-handling
(let ((now-playing-stats (icecast-now-playing *stream-base-url*))) (let ((now-playing-stats (icecast-now-playing *stream-base-url*)))
(if now-playing-stats (if now-playing-stats
(progn (progn
@ -62,7 +58,4 @@
(cdr (assoc :title now-playing-stats))) (cdr (assoc :title now-playing-stats)))
(progn (progn
(setf (header "Content-Type") "text/plain") (setf (header "Content-Type") "text/plain")
"Stream Offline"))) "Stream Offline")))))
(error (e)
(setf (header "Content-Type") "text/plain")
"Error loading stream info")))

View File

@ -1,5 +1,6 @@
(in-package #:rad-user) (in-package #:rad-user)
(define-module #:asteroid (define-module #:asteroid
(:use #:cl #:radiance #:asteroid.app-utils) (:use #:cl #:radiance #:lass #:r-clip #:asteroid.app-utils)
(:domain "asteroid")
(:export #:-main)) (:export #:-main))

View File

@ -296,3 +296,54 @@ function showMessage(message, type = 'info') {
function showError(message) { function showError(message) {
showMessage(message, 'error'); showMessage(message, 'error');
} }
// Password change handler
function changePassword(event) {
event.preventDefault();
const currentPassword = document.getElementById('current-password').value;
const newPassword = document.getElementById('new-password').value;
const confirmPassword = document.getElementById('confirm-password').value;
const messageDiv = document.getElementById('password-message');
// Client-side validation
if (newPassword.length < 8) {
messageDiv.textContent = 'New password must be at least 8 characters';
messageDiv.className = 'message error';
return false;
}
if (newPassword !== confirmPassword) {
messageDiv.textContent = 'New passwords do not match';
messageDiv.className = 'message error';
return false;
}
// Send request to API
const formData = new FormData();
formData.append('current-password', currentPassword);
formData.append('new-password', newPassword);
fetch('/api/asteroid/user/change-password', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.status === 'success' || (data.data && data.data.status === 'success')) {
messageDiv.textContent = 'Password changed successfully!';
messageDiv.className = 'message success';
document.getElementById('change-password-form').reset();
} else {
messageDiv.textContent = data.message || data.data?.message || 'Failed to change password';
messageDiv.className = 'message error';
}
})
.catch(error => {
console.error('Error changing password:', error);
messageDiv.textContent = 'Error changing password';
messageDiv.className = 'message error';
});
return false;
}

View File

@ -77,8 +77,7 @@
(if (and (stringp host-path) (if (and (stringp host-path)
(>= (length host-path) (length library-prefix)) (>= (length host-path) (length library-prefix))
(string= host-path library-prefix :end1 (length library-prefix))) (string= host-path library-prefix :end1 (length library-prefix)))
(concatenate 'string "/app/music/" (format nil "/app/music/~a" (subseq host-path (length library-prefix)))
(subseq host-path (length library-prefix)))
host-path))) host-path)))
(defun generate-m3u-playlist (track-ids output-path) (defun generate-m3u-playlist (track-ids output-path)
@ -183,9 +182,9 @@
(if (and (stringp docker-path) (if (and (stringp docker-path)
(>= (length docker-path) 11) (>= (length docker-path) 11)
(string= docker-path "/app/music/" :end1 11)) (string= docker-path "/app/music/" :end1 11))
(concatenate 'string (format nil "~a~a"
(namestring *music-library-path*) (namestring *music-library-path*)
(subseq docker-path 11)) (subseq docker-path 11))
docker-path)) docker-path))
(defun load-queue-from-m3u-file () (defun load-queue-from-m3u-file ()

View File

@ -166,11 +166,82 @@
<div class="controls"> <div class="controls">
<a href="/asteroid/admin/users" class="btn btn-primary">👥 Manage Users</a> <a href="/asteroid/admin/users" class="btn btn-primary">👥 Manage Users</a>
</div> </div>
<!-- Admin Password Reset Form -->
<div class="form-section" style="margin-top: 20px;">
<h4>🔒 Reset User Password</h4>
<form id="admin-reset-password-form" onsubmit="return resetUserPassword(event)">
<div class="form-group">
<label for="reset-username">Username:</label>
<input type="text" id="reset-username" name="username" required>
</div>
<div class="form-group">
<label for="reset-new-password">New Password:</label>
<input type="password" id="reset-new-password" name="new-password" required minlength="8">
</div>
<div class="form-group">
<label for="reset-confirm-password">Confirm Password:</label>
<input type="password" id="reset-confirm-password" name="confirm-password" required minlength="8">
</div>
<div id="reset-password-message" class="message"></div>
<button type="submit" class="btn btn-primary">Reset Password</button>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
// Admin password reset handler
function resetUserPassword(event) {
event.preventDefault();
const username = document.getElementById('reset-username').value;
const newPassword = document.getElementById('reset-new-password').value;
const confirmPassword = document.getElementById('reset-confirm-password').value;
const messageDiv = document.getElementById('reset-password-message');
// Client-side validation
if (newPassword.length < 8) {
messageDiv.textContent = 'New password must be at least 8 characters';
messageDiv.className = 'message error';
return false;
}
if (newPassword !== confirmPassword) {
messageDiv.textContent = 'Passwords do not match';
messageDiv.className = 'message error';
return false;
}
// Send request to API
const formData = new FormData();
formData.append('username', username);
formData.append('new-password', newPassword);
fetch('/api/asteroid/admin/reset-password', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.status === 'success' || (data.data && data.data.status === 'success')) {
messageDiv.textContent = 'Password reset successfully for user: ' + username;
messageDiv.className = 'message success';
document.getElementById('admin-reset-password-form').reset();
} else {
messageDiv.textContent = data.message || data.data?.message || 'Failed to reset password';
messageDiv.className = 'message error';
}
})
.catch(error => {
console.error('Error resetting password:', error);
messageDiv.textContent = 'Error resetting password';
messageDiv.className = 'message error';
});
return false;
}
</script> </script>
</body> </body>
</html> </html>

View File

@ -37,11 +37,6 @@
<button type="submit" class="btn btn-primary" style="width: 100%;">LOGIN</button> <button type="submit" class="btn btn-primary" style="width: 100%;">LOGIN</button>
</div> </div>
</form> </form>
<div class="panel" style="margin-top: 20px; text-align: center;">
<strong style="color: #ff6600;">Default Admin Credentials:</strong><br>
Username: <br><code style="color: #00ff00;">admin</code><br>
Password: <br><code style="color: #00ff00;">asteroid123</code>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -153,6 +153,28 @@
<!-- Profile Actions --> <!-- Profile Actions -->
<div class="admin-section"> <div class="admin-section">
<h2>⚙️ Profile Settings</h2> <h2>⚙️ Profile Settings</h2>
<!-- Change Password Form -->
<div class="form-section">
<h3>🔒 Change Password</h3>
<form id="change-password-form" onsubmit="return changePassword(event)">
<div class="form-group">
<label for="current-password">Current Password:</label>
<input type="password" id="current-password" name="current-password" required minlength="8">
</div>
<div class="form-group">
<label for="new-password">New Password:</label>
<input type="password" id="new-password" name="new-password" required minlength="8">
</div>
<div class="form-group">
<label for="confirm-password">Confirm New Password:</label>
<input type="password" id="confirm-password" name="confirm-password" required minlength="8">
</div>
<div id="password-message" class="message"></div>
<button type="submit" class="btn btn-primary">Change Password</button>
</form>
</div>
<div class="profile-actions"> <div class="profile-actions">
<button class="btn btn-primary" onclick="editProfile()">✏️ Edit Profile</button> <button class="btn btn-primary" onclick="editProfile()">✏️ Edit Profile</button>
<button class="btn btn-secondary" onclick="exportListeningData()">📊 Export Data</button> <button class="btn btn-secondary" onclick="exportListeningData()">📊 Export Data</button>

View File

@ -104,14 +104,46 @@
(defun reset-user-password (username new-password) (defun reset-user-password (username new-password)
"Reset a user's password" "Reset a user's password"
(let ((user (find-user-by-username username))) (let ((user (find-user-by-username username)))
(when user (if user
(let ((new-hash (hash-password new-password)) (handler-case
(user-id (gethash "_id" user))) (let ((new-hash (hash-password new-password))
(db:update "USERS" (user-id (gethash "_id" user)))
(db:query (:= "_id" user-id)) (format t "Resetting password for user: ~a (ID: ~a, type: ~a)~%" username user-id (type-of user-id))
`(("password-hash" ,new-hash))) (format t "New hash: ~a~%" new-hash)
(format t "Password reset for user: ~a~%" username) (format t "User hash table keys: ")
t)))) (maphash (lambda (k v) (format t "~a " k)) user)
(format t "~%")
(format t "Query: ~a~%" (db:query (:= "_id" user-id)))
(format t "Update data: ~a~%" `(("password-hash" ,new-hash)))
;; Try direct update with uppercase field name to match stored case
(format t "Attempting direct update with uppercase field name...~%")
(db:update "USERS"
(db:query (:= "_id" user-id))
`(("PASSWORD-HASH" ,new-hash)))
(format t "Update complete, verifying...~%")
;; Verify the update worked
(let ((updated-user (find-user-by-username username)))
(format t "Verification - fetching user again...~%")
(let ((updated-hash (gethash "PASSWORD-HASH" updated-user)))
(format t "Updated password hash in DB: ~a~%" updated-hash)
(format t "Expected hash: ~a~%" new-hash)
(let ((match (if (listp updated-hash)
(string= (first updated-hash) new-hash)
(string= updated-hash new-hash))))
(format t "Match: ~a~%" match)
(if match
(progn
(format t "Password reset successful for user: ~a~%" username)
t)
(progn
(format t "Password reset FAILED - hash didn't update~%")
nil))))))
(error (e)
(format t "Error resetting password for ~a: ~a~%" username e)
nil))
(progn
(format t "User not found: ~a~%" username)
nil))))
(defun user-has-role-p (user role) (defun user-has-role-p (user role)
"Check if user has the specified role" "Check if user has the specified role"