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:
2026-07-05 22:31:45 -04:00
parent 4ae7335b31
commit 72a8336087
8 changed files with 174 additions and 1 deletions
+21 -1
View File
@@ -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)