package services import ( "context" "fmt" "veza-backend-api/internal/database" "github.com/google/uuid" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" ) const passwordHistoryLimit = 5 // PasswordHistoryService manages password history to prevent reuse. // F014: ORIGIN_FEATURES_REGISTRY.md — stores last 5 password hashes. type PasswordHistoryService struct { db *database.Database logger *zap.Logger } // NewPasswordHistoryService creates a new password history service. func NewPasswordHistoryService(db *database.Database, logger *zap.Logger) *PasswordHistoryService { return &PasswordHistoryService{db: db, logger: logger} } // CheckReuse compares newPassword against the last 5 stored hashes. // Returns an error if the password was previously used. func (s *PasswordHistoryService) CheckReuse(ctx context.Context, userID uuid.UUID, newPassword string) error { rows, err := s.db.QueryContext(ctx, ` SELECT password_hash FROM password_history WHERE user_id = $1 ORDER BY created_at DESC LIMIT $2 `, userID, passwordHistoryLimit) if err != nil { // Table may not exist yet — not a blocking error s.logger.Debug("password_history query failed (table may not exist)", zap.Error(err)) return nil } defer rows.Close() for rows.Next() { var hash string if err := rows.Scan(&hash); err != nil { continue } if bcrypt.CompareHashAndPassword([]byte(hash), []byte(newPassword)) == nil { return fmt.Errorf("password was recently used — choose a different password") } } return nil } // Record stores the current password hash in history after a password change. // Automatically prunes entries beyond the limit. func (s *PasswordHistoryService) Record(ctx context.Context, userID uuid.UUID, passwordHash string) error { _, err := s.db.ExecContext(ctx, ` INSERT INTO password_history (id, user_id, password_hash, created_at) VALUES ($1, $2, $3, NOW()) `, uuid.New(), userID, passwordHash) if err != nil { s.logger.Warn("failed to record password history (table may not exist)", zap.Error(err)) return nil // non-blocking } // Prune old entries beyond limit _, _ = s.db.ExecContext(ctx, ` DELETE FROM password_history WHERE user_id = $1 AND id NOT IN ( SELECT id FROM password_history WHERE user_id = $1 ORDER BY created_at DESC LIMIT $2 ) `, userID, passwordHistoryLimit) return nil }