feat: add admin password change via /settings page
Backend: - DB: add UpdateAdminPasswordHash(id, hash) in queries_users.go - Auth: add ChangePassword(username, old, new) method that verifies old password, validates length >= 8, and stores bcrypt hash - Handlers: add handleChangePassword on PUT /api/auth/password (auth required) - Router: register route inside protected group Frontend: - api.ts: add changePassword(oldPassword, newPassword) - Settings.tsx: new page with form (current + new + confirm), client-side validation, success/error feedback - App.tsx: add /settings route - Layout.tsx: add 'Ajustes' nav item
This commit is contained in:
+21
-1
@@ -78,13 +78,33 @@ func (a *AuthService) EnsureAdmin(username, password string) (bool, error) {
|
||||
func (a *AuthService) Authenticate(username, password string) bool {
|
||||
admin, err := a.db.GetAdminByUsername(username)
|
||||
if err != nil {
|
||||
// Run a dummy hash comparison to reduce timing side-channels.
|
||||
_ = bcrypt.CompareHashAndPassword([]byte("$2a$10$invalidinvalidinvalidinvalidinvalidinvalidinvalidinv"), []byte(password))
|
||||
return false
|
||||
}
|
||||
return bcrypt.CompareHashAndPassword([]byte(admin.PasswordHash), []byte(password)) == nil
|
||||
}
|
||||
|
||||
func (a *AuthService) ChangePassword(username, oldPassword, newPassword string) error {
|
||||
if len(newPassword) < 8 {
|
||||
return fmt.Errorf("la nueva contraseña debe tener al menos 8 caracteres")
|
||||
}
|
||||
admin, err := a.db.GetAdminByUsername(username)
|
||||
if err != nil {
|
||||
return fmt.Errorf("admin no encontrado")
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(admin.PasswordHash), []byte(oldPassword)); err != nil {
|
||||
return fmt.Errorf("contraseña actual incorrecta")
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("hash password: %w", err)
|
||||
}
|
||||
if err := a.db.UpdateAdminPasswordHash(admin.ID, string(hash)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *AuthService) issueToken(username string) string {
|
||||
expiry := time.Now().Add(sessionTTL).Unix()
|
||||
payload := fmt.Sprintf("%s|%d", username, expiry)
|
||||
|
||||
@@ -10,6 +10,11 @@ type loginRequest struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type changePasswordRequest struct {
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if s.Auth == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "auth not configured")
|
||||
@@ -47,3 +52,30 @@ func (s *Server) handleAuthStatus(w http.ResponseWriter, r *http.Request) {
|
||||
"username": username,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
if s.Auth == nil {
|
||||
writeError(w, http.StatusServiceUnavailable, "auth not configured")
|
||||
return
|
||||
}
|
||||
username, ok := s.Auth.currentUser(r)
|
||||
if !ok {
|
||||
writeError(w, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
var req changePasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
if req.OldPassword == "" || req.NewPassword == "" {
|
||||
writeError(w, http.StatusBadRequest, "old_password and new_password are required")
|
||||
return
|
||||
}
|
||||
if err := s.Auth.ChangePassword(username, req.OldPassword, req.NewPassword); err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ func NewRouter(s *Server) chi.Router {
|
||||
protected.Post("/apply", s.handleApply)
|
||||
protected.Get("/apply/log", s.handleApplyLog)
|
||||
protected.Get("/system/status", s.handleSystemStatus)
|
||||
protected.Put("/auth/password", s.handleChangePassword)
|
||||
|
||||
protected.Get("/import/status", s.handleImportStatus)
|
||||
protected.Post("/import/samba", s.handleImportSamba)
|
||||
|
||||
Reference in New Issue
Block a user