]> gitweb.mndrdr.org Git - arelpe.git/commitdiff
2FA Support
authorAidan Cornelius-Bell <[email protected]>
Wed, 9 Oct 2024 21:53:29 +0000 (08:23 +1030)
committerAidan Cornelius-Bell <[email protected]>
Wed, 9 Oct 2024 21:53:29 +0000 (08:23 +1030)
26 files changed:
Gemfile
Gemfile.lock
app/controllers/application_controller.rb
app/controllers/two_factor_controller.rb [new file with mode: 0644]
app/controllers/users/sessions_controller.rb [new file with mode: 0644]
app/controllers/users/two_factor_authentication_controller.rb [new file with mode: 0644]
app/helpers/two_factor_helper.rb [new file with mode: 0644]
app/models/user.rb
app/views/devise/registrations/edit.html.erb
app/views/devise/sessions/new.html.erb
app/views/pubview/join.html.erb
app/views/two_factor/backup_codes.html.erb [new file with mode: 0644]
app/views/two_factor/create.html.erb [new file with mode: 0644]
app/views/two_factor/destroy.html.erb [new file with mode: 0644]
app/views/two_factor/new.html.erb [new file with mode: 0644]
app/views/users/two_factor_authentication/show.html.erb [new file with mode: 0644]
config/application.rb
config/credentials.yml.enc
config/initializers/.DS_Store [copied from app/assets/images/.DS_Store with 100% similarity]
config/initializers/devise.rb
config/routes.rb
db/migrate/20241009205529_add_two_factor_to_users.rb [new file with mode: 0644]
db/migrate/20241009210726_change_otp_secret_to_text.rb [new file with mode: 0644]
db/migrate/20241009212849_add_otp_backup_codes_to_users.rb [new file with mode: 0644]
db/schema.rb
test/controllers/two_factor_controller_test.rb [copied from test/controllers/subscriptions_controller_test.rb with 50% similarity]

diff --git a/Gemfile b/Gemfile
index 6213de99b9c291f3e2fd00930167a1affd9a9388..6bcc6bb92e0ca479607da1f36b143e7ffa7c329d 100644 (file)
--- a/Gemfile
+++ b/Gemfile
@@ -26,6 +26,9 @@ gem "hcaptcha"
 gem "stripe"
 # Scheduling
 gem "whenever", require: false
+# 2FA
+gem "devise-two-factor"
+gem "rqrcode"
 # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis]
 # gem "kredis"
 #api stuff
index d37a19df22ab3dbf652a6d6aea530e370dfc43d9..1b6e1d26da47d2e4c114b9fc9aa4e10dea17fe80 100644 (file)
@@ -95,6 +95,7 @@ GEM
     childprocess (5.1.0)
       logger (~> 1.5)
     chronic (0.10.2)
+    chunky_png (1.4.0)
     concurrent-ruby (1.3.4)
     connection_pool (2.4.1)
     crass (1.0.6)
@@ -109,6 +110,11 @@ GEM
       railties (>= 4.1.0)
       responders
       warden (~> 1.2.3)
+    devise-two-factor (6.0.0)
+      activesupport (~> 7.0)
+      devise (~> 4.0)
+      railties (~> 7.0)
+      rotp (~> 6.0)
     dotenv (3.1.2)
     drb (2.2.1)
     erubi (1.13.0)
@@ -246,7 +252,12 @@ GEM
       actionpack (>= 5.2)
       railties (>= 5.2)
     rexml (3.3.7)
+    rotp (6.3.0)
     rouge (4.3.0)
+    rqrcode (2.2.0)
+      chunky_png (~> 1.0)
+      rqrcode_core (~> 1.0)
+    rqrcode_core (1.2.0)
     rubocop (1.66.1)
       json (~> 2.3)
       language_server-protocol (>= 3.17.0)
@@ -331,6 +342,7 @@ DEPENDENCIES
   capybara
   debug
   devise
+  devise-two-factor
   dotenv
   hcaptcha
   httparty
@@ -342,6 +354,7 @@ DEPENDENCIES
   rails (~> 7.2.1)
   redcarpet
   rouge
+  rqrcode
   rubocop-rails-omakase
   selenium-webdriver
   sprockets-rails
index bfd3d2256aae62294b1abf42af3e76aace78cd24..f6356923107fd9aba98ec681a0cfce23540234ef 100644 (file)
@@ -8,5 +8,6 @@ class ApplicationController < ActionController::Base
   def configure_permitted_parameters
     devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name])
     devise_parameter_sanitizer.permit(:account_update, keys: [:first_name, :last_name])
+    devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
   end
 end
diff --git a/app/controllers/two_factor_controller.rb b/app/controllers/two_factor_controller.rb
new file mode 100644 (file)
index 0000000..ceed820
--- /dev/null
@@ -0,0 +1,36 @@
+# This controller is for setup of 2FA
+class TwoFactorController < ApplicationController
+  before_action :authenticate_user!
+
+  def new
+    current_user.generate_two_factor_secret_if_missing!
+    @qr_code_uri = current_user.two_factor_qr_code_uri
+    @qr_code = RQRCode::QRCode.new(@qr_code_uri).as_svg(
+      offset: 0,
+      color: '000',
+      shape_rendering: 'crispEdges',
+      module_size: 4
+    )
+    @manual_entry_code = current_user.otp_secret.upcase.scan(/.{4}/).join(' ')
+  end
+  
+  def create
+    if current_user.validate_and_consume_otp!(params[:otp_attempt])
+      current_user.enable_two_factor!
+      current_user.generate_otp_backup_codes!
+      redirect_to backup_codes_two_factor_path, notice: '2FA has been enabled. Please save your backup codes.'
+    else
+      flash.now[:alert] = 'Invalid OTP code.'
+      render :new
+    end
+  end
+  
+  def backup_codes
+    @backup_codes = current_user.otp_backup_codes
+  end
+
+  def destroy
+    current_user.disable_two_factor!
+    redirect_to root_path, notice: '2FA has been disabled.'
+  end
+end
\ No newline at end of file
diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb
new file mode 100644 (file)
index 0000000..2e42726
--- /dev/null
@@ -0,0 +1,19 @@
+class Users::SessionsController < Devise::SessionsController
+  def create
+       user = User.find_by_email(params[:user][:email])
+       
+       if user && user.valid_password?(params[:user][:password])
+         if user.otp_required_for_login?
+               session[:user_id] = user.id
+               redirect_to users_two_factor_authentication_path
+         else
+               sign_in(user)
+               set_flash_message!(:notice, :signed_in)
+               respond_with user, location: after_sign_in_path_for(user)
+         end
+       else
+         flash[:alert] = "Invalid email or password."
+         redirect_to new_user_session_path
+       end
+  end
+end
\ No newline at end of file
diff --git a/app/controllers/users/two_factor_authentication_controller.rb b/app/controllers/users/two_factor_authentication_controller.rb
new file mode 100644 (file)
index 0000000..474b544
--- /dev/null
@@ -0,0 +1,22 @@
+# This controller is for login with 2FA
+class Users::TwoFactorAuthenticationController < ApplicationController
+  def show
+       if session[:user_id].nil?
+         redirect_to new_user_session_path
+       end
+       @user = User.find_by(id: session[:user_id])
+       redirect_to new_user_session_path if @user.nil? || [email protected]_required_for_login?
+  end
+
+  def create
+       user = User.find_by(id: session[:user_id])
+       if user && user.validate_and_consume_otp!(params[:otp_attempt])
+         sign_in user
+         session.delete(:user_id)
+         redirect_to root_path, notice: 'Signed in successfully.'
+       else
+         flash.now[:alert] = 'Invalid two-factor code.'
+         render :show
+       end
+  end
+end
\ No newline at end of file
diff --git a/app/helpers/two_factor_helper.rb b/app/helpers/two_factor_helper.rb
new file mode 100644 (file)
index 0000000..773c882
--- /dev/null
@@ -0,0 +1,2 @@
+module TwoFactorHelper
+end
index d21d2edc56b5ac961d2e8a0eaabf820f2a5783e4..f5a2f92bd358a66b86470dc037b96b2c83441bbc 100644 (file)
@@ -3,7 +3,11 @@ class User < ApplicationRecord
   # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
   devise :database_authenticatable, :registerable,
          :recoverable, :rememberable, :validatable,
-         :confirmable, :lockable
+         :confirmable, :lockable, :two_factor_authenticatable, :two_factor_backupable,
+         otp_secret_encryption_key: Rails.application.credentials.active_record_encryption[:primary_key]
+         
+  encrypts :otp_secret
+  attr_accessor :otp_plain_secret
          
   validates :first_name, presence: true
   validates :last_name, presence: true
@@ -51,6 +55,47 @@ class User < ApplicationRecord
     send_welcome_email
   end
   
+  def generate_two_factor_secret_if_missing!
+    return unless otp_secret.nil?
+    update!(otp_secret: User.generate_otp_secret)
+  end
+  
+  def enable_two_factor!
+    update!(otp_required_for_login: true)
+  end
+  
+  def disable_two_factor!
+    update!(
+      otp_required_for_login: false,
+      otp_secret: nil,
+      otp_backup_codes: nil
+    )
+  end
+  
+  def two_factor_qr_code_uri
+    issuer = 'Your App Name'
+    label = "#{issuer}:#{email}"
+    otp_provisioning_uri(label, issuer: issuer)
+  end
+  
+  def generate_otp_backup_codes!
+    codes = []
+    10.times do
+      codes << SecureRandom.hex(8)
+    end
+    update!(otp_backup_codes: codes)
+  end
+  
+  # Getter and setter for otp_backup_codes
+  def otp_backup_codes
+    return [] if super.nil?
+    JSON.parse(super)
+  end
+  
+  def otp_backup_codes=(codes)
+    super(codes.to_json)
+  end
+  
   private
   def sync_with_buttondown
     ButtondownService.new.subscribe(self)
index 3d44108eeff5458e70d497b07b6b287b383a3673..91debf6f9673458e712e9cae6aeaeb4a6c1dbf25 100644 (file)
       <%= f.submit "Update" %>
     </div>
   <% end %>
+  <% if current_user.otp_required_for_login? %>
+    <%= link_to "Disable 2FA", two_factor_path, method: :delete, data: { confirm: "Are you sure you want to disable 2FA?" }, class: "button" %>
+  <% else %>
+    <%= link_to "Enable 2FA", new_two_factor_path, class: "button" %>
+  <% end %>
   <%= button_to "Log out", destroy_user_session_path, method: :delete %>
   <% if current_user&.admin? or current_user&.support_type? %>
   <h3>Account tools</h3>
index a471d0da6f6f6dd08ba2c031fa1e4b407fa83df6..156c43335fd5a9f66a212df3db22ef2eb1185523 100644 (file)
       <%= f.submit "Log in" %>
     </div>
   <% end %>
+  
+  <p>
+    If you have two-factor authentication enabled and were redirected here, 
+    please <%= link_to "enter your two-factor code", users_two_factor_authentication_path %>.
+  </p>
 <% end %>
 
 <%= render template: 'layouts/user_page_template' %>
\ No newline at end of file
index ddfa5ac150cd5dbe7b4f720f7feaee648bb03e3a..18c128d027b60d9bba24061e7ca2c5ac0def88b8 100644 (file)
   <div class="container">
        <%= image_tag "growchart.svg", class: "porter" %>
        <h3>Below you can join mind reader and sign up to receive new dispatches via email.</h3>
-       <p>Joining by email means you never have to visit this (admittedly beautiful) website ever again. You’ll receive new posts straight in your email inbox. Bookmarks, however, are sent in digest to subscribers only – so if you’d like to see those, come back or keep reading.</p>
-       <p>Optionally, once you’ve signed up and confirmed your email address you are able to subscribe to mind reader financially by entering any non $0 AUD amount you wish. You are under no obligation to do this! Paid subscribers may opt to receive a digest of bookmarks and a monthly update from me on the state of the world that regular members miss out on.</p>
+       <p>Joining by email means you never have to visit this (admittedly beautiful) website ever again. You’ll receive new posts straight in your email inbox. Bookmarks are, optionally, sent in digest to paid subscribers.</p>
+       <p>Optionally, once you’ve signed up and confirmed your email address you are able to subscribe to mind reader financially by entering any non $0 AUD amount you wish – you can do this just once, or annually. You are under no obligation to do this! Paid subscribers may opt to receive a digest of bookmarks and a monthly update from me on the state of the world that regular members miss out on.</p>
        <p>If you’re looking to cancel an existing membership or subscription, <%= link_to "please click here", subscriptions_path %>.</p>
        <%= image_tag "aidan_arrow.svg", class: "aidans_arrow" %>
-       <p>To get started with mind reader membership, first register for an account on this website. Don’t worry, I’ll guide you through each step of the process! Just click on that button below:</p>
-       <p><%= link_to "Get started, free forever", new_user_registration_path, class: "button" %></p>
+       <p>To get started with mind reader membership – <em>always free</em> – first register for an account on this website. Don’t worry, I’ll guide you through each step of the process! Just click on that button below:</p>
+       <p><%= link_to "Get started, free forever", new_user_registration_path, class: "button" %> <%= link_to "Log back in", new_user_session_path, class: "button" %></p>
+       <p><em>You can subscribe, unsubscribe, pay, cancel, and never hear from me again without ever contacting a human – note, though, if you pay for the service account deletion is manual to ensure all your details are securely erased. You can always one-click opt-out of communication and services.</em></p>
   </div>
 </div>
diff --git a/app/views/two_factor/backup_codes.html.erb b/app/views/two_factor/backup_codes.html.erb
new file mode 100644 (file)
index 0000000..8edfa9f
--- /dev/null
@@ -0,0 +1,19 @@
+<div class="container">
+  <h2>Your Backup Codes</h2>
+</div>
+
+<div class="post">
+  <div class="container">
+    <p>Please save these backup codes in a secure location. You will need them if you lose access to your authenticator app.</p>
+    
+    <ul>
+      <% @backup_codes.each do |code| %>
+           <li><%= code %></li>
+      <% end %>
+    </ul>
+    
+    <p>Each code can only be used once. If you use a backup code to sign in, a new set of codes will be generated.</p>
+    
+    <%= link_to "I have saved these codes", edit_user_registration_path, class: "button" %>
+  </div>
+</div>
\ No newline at end of file
diff --git a/app/views/two_factor/create.html.erb b/app/views/two_factor/create.html.erb
new file mode 100644 (file)
index 0000000..1b208b7
--- /dev/null
@@ -0,0 +1,2 @@
+<h1>TwoFactor#create</h1>
+<p>Find me in app/views/two_factor/create.html.erb</p>
diff --git a/app/views/two_factor/destroy.html.erb b/app/views/two_factor/destroy.html.erb
new file mode 100644 (file)
index 0000000..6ff97fe
--- /dev/null
@@ -0,0 +1,2 @@
+<h1>TwoFactor#destroy</h1>
+<p>Find me in app/views/two_factor/destroy.html.erb</p>
diff --git a/app/views/two_factor/new.html.erb b/app/views/two_factor/new.html.erb
new file mode 100644 (file)
index 0000000..033f411
--- /dev/null
@@ -0,0 +1,30 @@
+<div class="container">
+  <h2>Enable two-factor authentication</h2>
+  <%= link_to "Cancel two-factor setup...", edit_user_registration_path %>
+</div>
+
+<div class="post">
+  <div class="container">    
+    <p>Scan this QR code with your authenticator app:</p>
+    
+    <%= @qr_code.html_safe %>
+    
+    <p>Or manually enter this code:</p>
+    
+    <code><%= @manual_entry_code %></code>
+    
+    <p>Enter these details in your authenticator app:</p>
+    <ul>
+      <li><strong>Account:</strong> <%= current_user.email %></li>
+      <li><strong>Key:</strong> <%= @manual_entry_code %></li>
+      <li><strong>Time based:</strong> Yes</li>
+      <li><strong>Issuer:</strong> arelpe</li>
+    </ul>
+    
+    <%= form_tag two_factor_path, method: :post do %>
+      <%= label_tag :otp_attempt, "Enter the code from your authenticator app:" %>
+      <%= text_field_tag :otp_attempt, nil, autocomplete: 'off' %>
+      <%= submit_tag "Enable 2FA" %>
+    <% end %>
+  </div>
+</div>
\ No newline at end of file
diff --git a/app/views/users/two_factor_authentication/show.html.erb b/app/views/users/two_factor_authentication/show.html.erb
new file mode 100644 (file)
index 0000000..1cd9604
--- /dev/null
@@ -0,0 +1,16 @@
+<div class="container">
+       <h2>Two-Factor Authentication</h2>
+</div>
+
+<div class="post">
+       <div class="container">
+               <%= form_tag(users_two_factor_authentication_path, method: :post) do %>
+               <div>
+                       <%= label_tag :otp_attempt, "Enter your two-factor code" %>
+                       <%= text_field_tag :otp_attempt, nil, autocomplete: 'off', inputmode: 'numeric', pattern: '[0-9]*' %>
+               </div>
+               
+               <%= submit_tag "Verify" %>
+               <% end %>
+       </div>
+</div>
\ No newline at end of file
index 056949a700fb341b7f3df8d12bbeb52d3cf87446..da4c4d559709462b29e50bd39f7e3c6cdb3e1f6d 100644 (file)
@@ -35,5 +35,9 @@ module Arelpe
     #
     config.time_zone = "Australia/Adelaide"
     # config.eager_load_paths << Rails.root.join("extras")
+    #2FA Encryption
+    config.active_record.encryption.primary_key = Rails.application.credentials.active_record_encryption[:primary_key]
+    config.active_record.encryption.deterministic_key = Rails.application.credentials.active_record_encryption[:deterministic_key]
+    config.active_record.encryption.key_derivation_salt = Rails.application.credentials.active_record_encryption[:key_derivation_salt]
   end
 end
index 2370933de9f8c320b8bcbdc125cb459e67dd14eb..19f60ef0c4a4a9f119a7aa7ba3bf34a80fec195f 100644 (file)
@@ -1 +1 @@
-9kYVka9CFCj1yyYXajvTfcLa2BGRH44UtuAChP+O+StY/AtHrBFePFG9llTUtAi/OUIT/eIWgdlKRRGFBsq0lTlVucsqN+C+34Xs7YRz9nfRvrPb+liyWZIR0k81TkrU8CuN31aVlp/I2ZZeBEONbe/2ISbBKpXA1GScvveXCzhH93PZKJh2R1sDg+IGXAYeY39H71/pmJ0vwcmcHrrs/V5Fdvb6Ze17hBiS5NKuetEa9EvbLXzxTk8Vo8Bl9yqZkaoFaTvhiomraQUFP92TvCbjtzp7JESWU2uosyd0fAI+qwZXT/AIWLVfKSJ62oz9SygpJMvNJHi4SyuS9LcnN34ce9VHwQBB1/nf4lqqrJiXI7EP2LkISpprqTkqXOrf1mrklQ6uKNl/yI2Z/KoR8C/tE/Or--QXMk23jZC7frOfwO--M9QxrCetYHjpBOk9X/fZAQ==
\ No newline at end of file
+R6pLqAgOKbr+y07Is0RC9w3wQKbkr2h2KZO4RNpGCPAaE+X9ZK6i3AdB0XYfDiBRfvtj05EhYDJ+Yr72985yeeRhnppVqkwNK1mWp+SGfY+KVvaKI7HFYG8igI0LthDAdsyeoA2+ElpZgjHCCLqz36gRKSY/KEbT/y0NukHdQRzsWFHwqgY85vQK9gRfDBpM72h4iPwb9TOPvMtJuVwfaIAdcDY0OqaQpTlFIGjh/bT9njHuO2h4mJ6dCBJYoFR0+8oCpd8hHWHtxeB4bsqKspfxrNvFTYEDQUnNwJEbQGNl14577rhEshQ2WfgUVFGzTzfEpzQmFZZJtnWhJuJbgVaxUwH5STUKWEWNY5/gD2vgp1oj0aUO6eiyOjz5YCkUieR2RV3hVF+yvOMb9NTt2EBYjJGeoqngzKi/Hq0xnLHIRmkoFHPvuC1RHn0Ucg1Nez4cmWppoOK3xm/b0H2lKsWi97vkkii38lqtfmmRFtsdXf4AvFOQ2NWpFlLKLwHT87/bU32XIzPObwErw/wv9irxrBMAlDDqOTqtqd6k3liddoNGCqUqHLxSC0IoH/G2tdg1PzoKBvtIVyjtctYtUnta1vgjpz727WCSSYO6gDTOxTkRmwc++DcBAzOJFkYSz8gFm02TjHaBhzXQeqw=--fmjiwM3a6xuvqqTn--wwZXJd1Zcs5AJV/lQbpCtw==
\ No newline at end of file
index 545e8c69aea555e5d885285356ff04c71a0a174a..9d0139e9d017811df36b7b1f836c07bb7cab2aab 100644 (file)
@@ -209,7 +209,7 @@ Devise.setup do |config|
   # Number of authentication tries before locking an account if lock_strategy
   # is failed attempts.
   config.maximum_attempts = 6
-
+  
   # Time interval to unlock the account if :time is enabled as unlock_strategy.
   # config.unlock_in = 1.hour
 
index efa8e9f24afed85592a69919fda6bd58e26e40d8..9678326904631997867415fedcb6896c8ced8ef9 100644 (file)
@@ -10,7 +10,7 @@ Rails.application.routes.draw do
     end
   end
   
-resources :mailing_lists, only: [:index] do
+  resources :mailing_lists, only: [:index] do
     collection do
       post 'subscribe'
       delete 'unsubscribe'
@@ -18,13 +18,22 @@ resources :mailing_lists, only: [:index] do
       post 'resync_from_buttondown'
     end
   end
+  resource :two_factor, only: [:new, :create, :destroy], controller: 'two_factor' do
+    get 'backup_codes', on: :member
+  end
   resources :subscriptions, only: [:new, :create, :index]
   resources :pages
   get 'importer', to: 'posts#importer'
   get 'export', to: 'posts#export'
   post 'import', to: 'posts#import'
   resources :api_keys
-  devise_for :users, controllers: { registrations: 'users/registrations', confirmations: 'users/confirmations' }
+  devise_for :users, controllers: { 
+    registrations: 'users/registrations', 
+    confirmations: 'users/confirmations',
+    sessions: 'users/sessions'
+  }
+  get 'users/two_factor_authentication', to: 'users/two_factor_authentication#show'
+  post 'users/two_factor_authentication', to: 'users/two_factor_authentication#create'
   resources :posts
   get '/feed', to: 'pubview#rss', as: 'rss', defaults: { format: 'rss' }
   get '/feed/dispatches', to: 'pubview#dispatches_rss', as: 'dispatches_rss', defaults: { format: 'rss' }
diff --git a/db/migrate/20241009205529_add_two_factor_to_users.rb b/db/migrate/20241009205529_add_two_factor_to_users.rb
new file mode 100644 (file)
index 0000000..2984da9
--- /dev/null
@@ -0,0 +1,6 @@
+class AddTwoFactorToUsers < ActiveRecord::Migration[7.2]
+  def change
+    add_column :users, :otp_secret, :string
+    add_column :users, :otp_required_for_login, :boolean
+  end
+end
diff --git a/db/migrate/20241009210726_change_otp_secret_to_text.rb b/db/migrate/20241009210726_change_otp_secret_to_text.rb
new file mode 100644 (file)
index 0000000..8d6bbeb
--- /dev/null
@@ -0,0 +1,9 @@
+class ChangeOtpSecretToText < ActiveRecord::Migration[7.2]
+  def up
+    change_column :users, :otp_secret, :text
+  end
+
+  def down
+    change_column :users, :otp_secret, :string
+  end
+end
diff --git a/db/migrate/20241009212849_add_otp_backup_codes_to_users.rb b/db/migrate/20241009212849_add_otp_backup_codes_to_users.rb
new file mode 100644 (file)
index 0000000..ce8ca7e
--- /dev/null
@@ -0,0 +1,5 @@
+class AddOtpBackupCodesToUsers < ActiveRecord::Migration[7.2]
+  def change
+    add_column :users, :otp_backup_codes, :text
+  end
+end
index 8275001a33c864e058072a9fab3640af4a4aa466..ddc1cd4166e435b6954b40d9c582ecddf8aeaab5 100644 (file)
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema[7.2].define(version: 2024_10_08_010447) do
+ActiveRecord::Schema[7.2].define(version: 2024_10_09_212849) do
   create_table "api_keys", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
     t.string "key"
     t.datetime "created_at", null: false
@@ -69,6 +69,9 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_08_010447) do
     t.string "buttondown_status", default: "unactivated"
     t.string "subscription_type"
     t.string "support_type"
+    t.text "otp_secret"
+    t.boolean "otp_required_for_login"
+    t.text "otp_backup_codes"
     t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
     t.index ["email"], name: "index_users_on_email", unique: true
     t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
similarity index 50%
copy from test/controllers/subscriptions_controller_test.rb
copy to test/controllers/two_factor_controller_test.rb
index 3958a5a9b61355ff1f37b73d0bdf7cde3df580ed..4ef2cf780008f43941f8f8fa8f3e561e0b987d0c 100644 (file)
@@ -1,18 +1,18 @@
 require "test_helper"
 
-class SubscriptionsControllerTest < ActionDispatch::IntegrationTest
+class TwoFactorControllerTest < ActionDispatch::IntegrationTest
   test "should get new" do
-    get subscriptions_new_url
+    get two_factor_new_url
     assert_response :success
   end
 
   test "should get create" do
-    get subscriptions_create_url
+    get two_factor_create_url
     assert_response :success
   end
 
-  test "should get view" do
-    get subscriptions_view_url
+  test "should get destroy" do
+    get two_factor_destroy_url
     assert_response :success
   end
 end