feat: add password management, reading persistence on feed switch, and spin animation

This commit is contained in:
2026-08-24 21:53:54 -07:00
parent d7ed8876dc
commit 5ea176d5ad
12 changed files with 424 additions and 76 deletions
+23
View File
@@ -84,6 +84,29 @@ func (s *AuthService) Authenticate(username, password string) (string, error) {
return tokenString, nil
}
func (s *AuthService) UpdatePassword(userID, oldPassword, newPassword string) error {
var currentHash string
err := s.db.QueryRow("SELECT password_hash FROM users WHERE id = ?", userID).Scan(&currentHash)
if err != nil {
return errors.New("user not found")
}
if err := bcrypt.CompareHashAndPassword([]byte(currentHash), []byte(oldPassword)); err != nil {
return errors.New("incorrect current password")
}
if len(strings.TrimSpace(newPassword)) < 6 {
return errors.New("new password must be at least 6 characters long")
}
newHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("failed to hash new password: %w", err)
}
return s.db.UpdateUserPassword(userID, string(newHash))
}
func (s *AuthService) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
+87
View File
@@ -0,0 +1,87 @@
package auth
import (
"os"
"path/filepath"
"testing"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/config"
"git.pengzhan.dev/haopengzhan/rsspipe/internal/db"
)
func TestAuthService(t *testing.T) {
tempDir, err := os.MkdirTemp("", "rsspipe-auth-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer func() {
_ = os.RemoveAll(tempDir)
}()
dbPath := filepath.Join(tempDir, "auth_test.db")
database, err := db.InitDB(dbPath)
if err != nil {
t.Fatalf("InitDB failed: %v", err)
}
defer func() {
_ = database.Close()
}()
cfg := &config.Config{
AdminUsername: "admin",
AdminPassword: "initial_password_123",
JWTSecret: "super-secret-jwt-key-for-test",
}
authService := NewAuthService(database, cfg)
// 1. Bootstrap Admin
if err := authService.BootstrapAdmin(); err != nil {
t.Fatalf("BootstrapAdmin failed: %v", err)
}
// 2. Authenticate with initial password
token, err := authService.Authenticate("admin", "initial_password_123")
if err != nil {
t.Fatalf("Authenticate failed: %v", err)
}
if token == "" {
t.Fatalf("expected non-empty token")
}
// 3. Validate Token
claims, err := authService.ValidateToken(token)
if err != nil {
t.Fatalf("ValidateToken failed: %v", err)
}
if claims.Username != "admin" {
t.Errorf("expected username admin, got %s", claims.Username)
}
// 4. Update Password
if err := authService.UpdatePassword(claims.UserID, "wrong_old_pwd", "new_secure_pwd_456"); err == nil {
t.Errorf("expected error with incorrect old password")
}
if err := authService.UpdatePassword(claims.UserID, "initial_password_123", "short"); err == nil {
t.Errorf("expected error with too short new password")
}
if err := authService.UpdatePassword(claims.UserID, "initial_password_123", "new_secure_pwd_456"); err != nil {
t.Fatalf("UpdatePassword failed: %v", err)
}
// 5. Authenticate with new password
newToken, err := authService.Authenticate("admin", "new_secure_pwd_456")
if err != nil {
t.Fatalf("Authenticate with new password failed: %v", err)
}
if newToken == "" {
t.Fatalf("expected non-empty token for new password")
}
// 6. Old password should now fail
if _, err := authService.Authenticate("admin", "initial_password_123"); err == nil {
t.Errorf("expected failure when authenticating with old password")
}
}