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
+
+
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))