Tengo una página de verificación de dos factores, se muestra una clave secreta (texto cifrado) y ya tengo clipboard.js instalado en mi aplicación.

enter image description here

Me pregunto cómo es posible crear un botón para copiar esa clave secreta.

= simple_form_for @google_auth, as: 'google_auth', url: verify_google_auth_path do |f|
  h4 = t('.step-1')
  p
    span = t('.download-app')
    span == t('.guide-link')

  h4 = t('.step-2')
  p: span = t('.scan-qr-code')

  = f.input :uri do
    = qr_tag(@google_auth.uri)

  = f.input :otp_secret do
    .input-group
      = f.input_field :otp_secret, class: 'upcase', readonly: true
      span.input-group-btn
        a.btn.btn-default href='#{verify_google_auth_path(:app, refresh: true)}'
          i.fa.fa-refresh

  h4 = t('.step-3')
  p: span = t('.enter-passcode')

  = f.input :otp

  hr.split
  = f.button :wrapped, t('.submit'), cancel: settings_path

= content_for :guide do
  ul.list-unstyled
    li: a target='_blank' href='https://apps.apple.com/br/app/authy/id494168017'
      i.fa.fa-apple
      span = t('.ios')
    li: a target='_blank' href='https://play.google.com/store/apps/details?id=com.authy.authy'
      i.fa.fa-android
      span = t('.android')

Traté de hacer esto, pero no funcionó:

a.btn.btn-default data-clipboard-action='copy' data-clipboard-target=':otp_secret'
  i.fa.fa-clipboard

En el ejemplo anterior, solo copia el texto otp_secret puro.

Spec \ models \ two_factor \ app_spec.rb:

require 'spec_helper'

describe TwoFactor::App do
  let(:member) { create :member }
  let(:app) { member.app_two_factor  }

  describe "generate code" do
    subject { app }

    its(:otp_secret) { should_not be_blank }
  end

  describe '#refresh' do
    context 'inactivated' do
      it {
        orig_otp_secret = app.otp_secret.dup
        app.refresh!
        expect(app.otp_secret).not_to eq(orig_otp_secret)
      }
    end

    context 'activated' do
      subject { create :two_factor_app, activated: true }

      it {
        orig_otp_secret = subject.otp_secret.dup
        subject.refresh!
        expect(subject.otp_secret).to eq(orig_otp_secret)
      }
    end
  end

  describe 'uniq validate' do
    let(:member) { create :member }

    it "reject duplicate creation" do
      duplicate = TwoFactor.new app.attributes
      expect(duplicate).not_to be_valid
    end
  end

  describe 'self.fetch_by_type' do
    it "return nil for wrong type" do
      expect(TwoFactor.by_type(:foobar)).to be_nil
    end

    it "create new one by type" do
      expect {
        expect(app).not_to be_nil
      }.to change(TwoFactor::App, :count).by(1)
    end

    it "retrieve exist one instead of creating" do
      two_factor = member.app_two_factor
      expect(member.app_two_factor).to eq(two_factor)
    end
  end

  describe '#active!' do
    subject { member.app_two_factor }
    before { subject.active! }

    its(:activated?) { should be_true }
  end

  describe '#deactive!' do
    subject { create :two_factor_app, activated: true }
    before { subject.deactive! }

    its(:activated?) { should_not be_true }
  end


  describe '.activated' do
    before { create :member, :app_two_factor_activated }

    it "should has activated" do
      expect(TwoFactor.activated?).to be_true
    end
  end

  describe 'send_notification_mail' do
    let(:mail) { ActionMailer::Base.deliveries.last }

    describe "activated" do
      before { app.active! }

      it { expect(mail.subject).to match('Google authenticator activated') }
    end

    describe "deactived" do
      let(:member) { create :member, :app_two_factor_activated }
      before { app.deactive! }

      it { expect(mail.subject).to match('Google authenticator deactivated') }
    end
  end

end

App.rb:

class TwoFactor::App < ::TwoFactor

  def verify?
    return false if otp_secret.blank?

    rotp = ROTP::TOTP.new(otp_secret)

    if rotp.verify(otp)
      touch(:last_verify_at)
      true
    else
      errors.add :otp, :invalid
      false
    end
  end

  def uri
    totp = ROTP::TOTP.new(otp_secret)
    totp.provisioning_uri(member.email) + "&issuer=#{ENV['URL_HOST']}"
  end

  def now
    ROTP::TOTP.new(otp_secret).now
  end

  def refresh!
    return if activated?
    super
  end

  private

  def gen_code
    self.otp_secret = ROTP::Base32.random_base32
    self.refreshed_at = Time.new
  end

  def send_notification
    return if not self.activated_changed?

    if self.activated
      MemberMailer.google_auth_activated(member.id).deliver
    else
      MemberMailer.google_auth_deactivated(member.id).deliver
    end
  end

end

EDITAR: app \ models \ two_factor.rb:

class TwoFactor < ActiveRecord::Base
  belongs_to :member

  before_validation :gen_code, on: :create
  after_update :send_notification

  validates_presence_of :member, :otp_secret, :refreshed_at

  attr_accessor :otp

  SUBCLASS = ['app', 'sms', 'email', 'wechat']

  validates_uniqueness_of :type, scope: :member_id

  scope :activated, -> { where(activated: true) }
  scope :require_signin, -> { where(require_signin: 1) }

  class << self
    def by_type(type)
      return if not SUBCLASS.include?(type.to_s)

      klass = "two_factor/#{type}".camelize.constantize
      klass.find_or_create_by(type: klass.name)
    end

    def activated?
      activated.any?
    end

    def require_signin?
      require_signin.any?
    end
  end

  def verify?
    msg = "#{self.class.name}#verify? is not implemented."
    raise NotImplementedError.new(msg)
  end

  def expired?
    Time.now >= 30.minutes.since(refreshed_at)
  end

  def refresh!
    gen_code
    save
  end

  def active!
    update activated: true, last_verify_at: Time.now
  end

  def set_require_signin
    update require_signin: 1
  end

  def reset_require_signin
    update require_signin: nil
  end

  def deactive!
    update activated: false, require_signin: nil
  end

  private

  def gen_code
    msg = "#{self.class.name}#gen_code is not implemented."
    raise NotImplementedError.new(msg)
  end

  def send_notification
    msg = "#{self.class.name}#send_notification is not implemented."
    raise NotImplementedError.new(msg)
  end

end
1
Spero Coin 11 feb. 2020 a las 14:15

2 respuestas

La mejor respuesta

Logré resolverlo en base a las sugerencias de nuestro amigo @lacostenycoder.

Solo había una necesidad de cambiar incluso en el archivo show.html.slim, con este aspecto:

= simple_form_for @google_auth, as: 'google_auth', url: verify_google_auth_path do |f|
  h4 = t('.step-1')
  p
    span = t('.download-app')
    span == t('.guide-link')

  h4 = t('.step-2')
  p: span = t('.scan-qr-code')

  = f.input :uri do
    = qr_tag(@google_auth.uri)

  = f.input :otp_secret do
    .input-group
      .form-control.form-control-static = @google_auth.otp_secret
      .input-group
          a.btn.btn-default href="javascript:void(0)" data-clipboard-text = @google_auth.otp_secret
            i.fa.fa-clipboard
          a.btn.btn-default href='#{verify_google_auth_path(:app, refresh: true)}'
            i.fa.fa-refresh

  h4 = t('.step-3')
  p: span = t('.enter-passcode')

  = f.input :otp

  hr.split
  = f.button :wrapped, t('.submit'), cancel: settings_path

= content_for :guide do
  ul.list-unstyled
    li: a target='_blank' href='https://apps.apple.com/br/app/authy/id494168017'
      i.fa.fa-apple
      span = t('.ios')
    li: a target='_blank' href='https://play.google.com/store/apps/details?id=com.authy.authy'
      i.fa.fa-android
      span = t('.android')
0
Spero Coin 11 feb. 2020 a las 21:52

Lo que parece que estás tratando de hacer es copiar el valor de un campo de entrada (que ha sido poblado por otro código que tienes) en el portapapeles del sistema. Necesitas usar javascript para hacer esto, si tienes jquery esto debería funcionar.

Para su delgado necesita una identificación para apuntarlo

a.btn.btn-default id= "copy"
  i.fa.fa-clipboard

Intente agregar una identificación al elemento de entrada desde el que desea copiar

= f.input_field :otp_secret, class: 'upcase', id: "secret", readonly: true 

Ahora intenta cambiar esto y ver si funciona.

a.btn.btn-default data-clipboard-action='copy' data-clipboard-target='secret'
  i.fa.fa-clipboard

Además, en algún lugar de su JavaScript, tendrá que orientar el evento de clip con algo como esto:

new ClipboardJS('#secret');

Vea el ejemplo aquí https://jsfiddle.net/ec3ywrzd/

Entonces necesitará este javascript para cargar en su html. Pero deberá poder orientar el campo de cifrado, en este ejemplo estoy usando id="secret". No estoy seguro si el código OTP que tiene genera su propia ID o ahora, por lo que es posible que deba inspeccionar su dom para descubrir cómo apuntarlo para agregar una ID. Puede intentar agregar una identificación aquí:

= f.input_field :otp_secret, class: 'upcase', id: "secret", readonly: true 

De lo contrario, tendrá que usar otros selectores de consulta para orientarlo. Pero es posible que no necesite clipboardjs en absoluto.

Aquí hay un ejemplo básico en jsfiddle para probarlo, simplemente puede agregar cualquier cadena al campo de entrada. Deberá agregar esto a un archivo JS que se cargará según su diseño de vista, es decir, application.js

$(document).ready(function() {
  $('#copy').click(function(){
    $('#secret').select();
    document.execCommand('copy');
    alert("copied!");
  })
})

También puede ver las respuestas a esta pregunta

2
lacostenycoder 11 feb. 2020 a las 18:01