Compare commits
13 Commits
043c0d8610
...
ffd178c555
| Author | SHA1 | Date |
|---|---|---|
|
|
ffd178c555 | |
|
|
270c0ad679 | |
|
|
1665efe1e1 | |
|
|
40a49c1c25 | |
|
|
30b2d88f6a | |
|
|
3dd9c2d469 | |
|
|
b9b3feda6b | |
|
|
69b0b2ca9e | |
|
|
b80dea5a08 | |
|
|
5882141cfa | |
|
|
85881b8fb6 | |
|
|
4c99ded7f0 | |
|
|
c58c8a255c |
2
Makefile
2
Makefile
|
|
@ -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 \
|
||||||
|
|
|
||||||
32
TODO.org
32
TODO.org
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)))))))
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -31,5 +31,19 @@
|
||||||
(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~%"))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,6 @@
|
||||||
("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))
|
||||||
|
|
|
||||||
|
|
@ -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")))
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,7 +182,7 @@
|
||||||
(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))
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
(handler-case
|
||||||
(let ((new-hash (hash-password new-password))
|
(let ((new-hash (hash-password new-password))
|
||||||
(user-id (gethash "_id" user)))
|
(user-id (gethash "_id" user)))
|
||||||
|
(format t "Resetting password for user: ~a (ID: ~a, type: ~a)~%" username user-id (type-of user-id))
|
||||||
|
(format t "New hash: ~a~%" new-hash)
|
||||||
|
(format t "User hash table keys: ")
|
||||||
|
(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:update "USERS"
|
||||||
(db:query (:= "_id" user-id))
|
(db:query (:= "_id" user-id))
|
||||||
`(("password-hash" ,new-hash)))
|
`(("PASSWORD-HASH" ,new-hash)))
|
||||||
(format t "Password reset for user: ~a~%" username)
|
(format t "Update complete, verifying...~%")
|
||||||
t))))
|
;; 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"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue