Add password change functionality
- Add POST /api/auth/password API endpoint
- Requires authentication and current password verification
- Invalidates all other sessions after password change
- Keeps current session active
- Add window.changePassword() console function
- Matches existing login flow pattern
- Usage: changePassword("current", "new")
- Add 'lookbook set-password' CLI command
- Interactive password reset (no current password required)
- Useful for recovery scenarios
- Invalidates all sessions
- Add session.QDeleteAllExcept() and session.QDeleteAll()
- Support for invalidating sessions after password change
This commit is contained in:
parent
5b472de209
commit
523831cb8d
6 changed files with 178 additions and 6 deletions
|
|
@ -71,3 +71,17 @@ func QDeleteExpired(ctx context.Context, db *sql.DB) error {
|
|||
_, err := db.ExecContext(ctx, query)
|
||||
return err
|
||||
}
|
||||
|
||||
// QDeleteAllExcept deletes all sessions except the one with the given session ID.
|
||||
func QDeleteAllExcept(ctx context.Context, db *sql.DB, exceptSessionID string) error {
|
||||
query := `DELETE FROM session WHERE session_id != $1`
|
||||
_, err := db.ExecContext(ctx, query, exceptSessionID)
|
||||
return err
|
||||
}
|
||||
|
||||
// QDeleteAll deletes all sessions.
|
||||
func QDeleteAll(ctx context.Context, db *sql.DB) error {
|
||||
query := `DELETE FROM session`
|
||||
_, err := db.ExecContext(ctx, query)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,11 @@ type loginResponse struct {
|
|||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type changePasswordRequest struct {
|
||||
CurrentPassword string `json:"currentPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
|
||||
// HandleLogin handles POST /api/auth/login
|
||||
// If no password is set, it sets the password. Otherwise, it verifies the password.
|
||||
func HandleLogin(rc *RequestContext, w http.ResponseWriter, r *http.Request) error {
|
||||
|
|
@ -126,6 +131,55 @@ func HandleAuthStatus(rc *RequestContext, w http.ResponseWriter, r *http.Request
|
|||
})
|
||||
}
|
||||
|
||||
// HandleChangePassword handles POST /api/auth/password
|
||||
func HandleChangePassword(rc *RequestContext, w http.ResponseWriter, r *http.Request) error {
|
||||
if !rc.RequireAdmin(w) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var req changePasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
}
|
||||
|
||||
if req.CurrentPassword == "" || req.NewPassword == "" {
|
||||
return writeJSON(w, http.StatusBadRequest, map[string]string{"error": "both passwords required"})
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Get current password hash
|
||||
adm, err := admin.QGet(ctx, rc.DB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
if err := bcrypt.CompareHashAndPassword(adm.PasswordHash, []byte(req.CurrentPassword)); err != nil {
|
||||
return writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid current password"})
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update password
|
||||
if err := admin.QSetPassword(ctx, rc.DB, hash); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Invalidate all other sessions (keep current session)
|
||||
cookie, _ := r.Cookie("session_id")
|
||||
if cookie != nil {
|
||||
session.QDeleteAllExcept(ctx, rc.DB, cookie.Value)
|
||||
}
|
||||
|
||||
return writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func generateSessionID() (string, error) {
|
||||
b := make([]byte, 32)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,28 @@ window.logout = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
window.changePassword = async (currentPassword, newPassword) => {
|
||||
if (!currentPassword || !newPassword) {
|
||||
console.error('Usage: changePassword("current-password", "new-password")');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await fetch("/api/auth/password", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ currentPassword, newPassword }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
console.log("Password changed successfully!");
|
||||
} else {
|
||||
console.error(data.error || "Failed to change password");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Password change error:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// Modal functions
|
||||
function showAddModal() {
|
||||
document.getElementById("add-modal").classList.add("active");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue