From 55a9ae816ff99af1ebe753904b09817ac5b2cede Mon Sep 17 00:00:00 2001 From: Anders Holck Date: Mon, 25 May 2026 08:44:15 +0200 Subject: [PATCH] Add change password (Preferences > Change Password) --- frontend/src/App.vue | 26 ++++++++++++++++++++++++++ internal/api/handlers.go | 28 ++++++++++++++++++++++++++++ internal/api/router.go | 1 + 3 files changed, 55 insertions(+) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index c441295..64d2771 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -164,6 +164,16 @@ +

Change Password

+
+
+ + + +
+

{{ pwMsg }}

+
+

Two-Factor Authentication

@@ -293,6 +303,11 @@ const totpUri = ref('') const totpSecret = ref('') const totpCode = ref('') +// Password change +const pwCurrent = ref('') +const pwNew = ref('') +const pwMsg = ref('') + // Git remotes const gitRemotes = ref([]) const newRemote = ref({ name: '', url: '' }) @@ -527,6 +542,17 @@ function applyThemeFromPrefs() { savePrefs() } +async function changePassword() { + try { + await api('/api/auth/change-password', { current_password: pwCurrent.value, new_password: pwNew.value }) + pwMsg.value = 'Password changed successfully' + pwCurrent.value = '' + pwNew.value = '' + } catch (e) { + pwMsg.value = 'Failed — check current password' + } +} + // ─── TOTP ──────────────────────────────────────────────────────────────────── async function setupTOTP() { diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 8906259..db7bfc1 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -92,6 +92,34 @@ func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { writeJSON(w, 200, map[string]string{"status": "ok"}) } +func (s *Server) handleChangePassword(w http.ResponseWriter, r *http.Request) { + var req struct { + CurrentPassword string `json:"current_password"` + NewPassword string `json:"new_password"` + } + if err := decodeBody(r, &req); err != nil || req.CurrentPassword == "" || req.NewPassword == "" { + writeJSON(w, 400, map[string]string{"error": "current_password and new_password required"}) + return + } + + userID := getUserID(r) + var hash string + s.db.QueryRow("SELECT password_hash FROM users WHERE id = ?", userID).Scan(&hash) + if !auth.CheckPassword(hash, req.CurrentPassword) { + writeJSON(w, 401, map[string]string{"error": "current password is incorrect"}) + return + } + + newHash, err := auth.HashPassword(req.NewPassword) + if err != nil { + writeJSON(w, 500, map[string]string{"error": "failed to hash password"}) + return + } + + s.db.Exec("UPDATE users SET password_hash = ?, updated_at = datetime('now') WHERE id = ?", newHash, userID) + writeJSON(w, 200, map[string]string{"status": "password changed"}) +} + // ─── Users ─────────────────────────────────────────────────────────────────── func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) { diff --git a/internal/api/router.go b/internal/api/router.go index f165844..38d961e 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -27,6 +27,7 @@ func NewRouter(db *sql.DB, dataDir, secret string) http.Handler { // Users (admin) mux.HandleFunc("POST /api/users/create", s.requireAdmin(s.handleCreateUser)) mux.HandleFunc("POST /api/users/list", s.requireAdmin(s.handleListUsers)) + mux.HandleFunc("POST /api/auth/change-password", s.requireAuth(s.handleChangePassword)) // Files mux.HandleFunc("POST /api/files/list", s.requireAuth(s.handleListFiles))