quo_vadis 2.0.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +7 -0
  4. data/Gemfile +0 -3
  5. data/README.md +4 -5
  6. data/lib/quo_vadis/version.rb +1 -1
  7. data/quo_vadis.gemspec +5 -3
  8. data/test/dummy/README.markdown +1 -0
  9. data/test/dummy/Rakefile +3 -0
  10. data/test/dummy/app/controllers/application_controller.rb +2 -0
  11. data/test/dummy/app/controllers/articles_controller.rb +17 -0
  12. data/test/dummy/app/controllers/sign_ups_controller.rb +42 -0
  13. data/test/dummy/app/controllers/users_controller.rb +25 -0
  14. data/test/dummy/app/models/application_record.rb +3 -0
  15. data/test/dummy/app/models/article.rb +3 -0
  16. data/test/dummy/app/models/person.rb +6 -0
  17. data/test/dummy/app/models/user.rb +6 -0
  18. data/test/dummy/app/views/articles/also_secret.html.erb +1 -0
  19. data/test/dummy/app/views/articles/index.html.erb +1 -0
  20. data/test/dummy/app/views/articles/secret.html.erb +1 -0
  21. data/test/dummy/app/views/articles/very_secret.html.erb +2 -0
  22. data/test/dummy/app/views/layouts/application.html.erb +46 -0
  23. data/test/dummy/app/views/quo_vadis/confirmations/edit.html.erb +10 -0
  24. data/test/dummy/app/views/quo_vadis/confirmations/index.html.erb +5 -0
  25. data/test/dummy/app/views/quo_vadis/confirmations/new.html.erb +16 -0
  26. data/test/dummy/app/views/quo_vadis/logs/index.html.erb +28 -0
  27. data/test/dummy/app/views/quo_vadis/mailer/account_confirmation.text.erb +4 -0
  28. data/test/dummy/app/views/quo_vadis/mailer/email_change_notification.text.erb +8 -0
  29. data/test/dummy/app/views/quo_vadis/mailer/identifier_change_notification.text.erb +8 -0
  30. data/test/dummy/app/views/quo_vadis/mailer/password_change_notification.text.erb +8 -0
  31. data/test/dummy/app/views/quo_vadis/mailer/password_reset_notification.text.erb +8 -0
  32. data/test/dummy/app/views/quo_vadis/mailer/recovery_codes_generation_notification.text.erb +8 -0
  33. data/test/dummy/app/views/quo_vadis/mailer/reset_password.text.erb +4 -0
  34. data/test/dummy/app/views/quo_vadis/mailer/totp_reuse_notification.text.erb +6 -0
  35. data/test/dummy/app/views/quo_vadis/mailer/totp_setup_notification.text.erb +8 -0
  36. data/test/dummy/app/views/quo_vadis/mailer/twofa_deactivated_notification.text.erb +8 -0
  37. data/test/dummy/app/views/quo_vadis/password_resets/edit.html.erb +25 -0
  38. data/test/dummy/app/views/quo_vadis/password_resets/index.html.erb +5 -0
  39. data/test/dummy/app/views/quo_vadis/password_resets/new.html.erb +12 -0
  40. data/test/dummy/app/views/quo_vadis/passwords/edit.html.erb +30 -0
  41. data/test/dummy/app/views/quo_vadis/recovery_codes/challenge.html.erb +11 -0
  42. data/test/dummy/app/views/quo_vadis/recovery_codes/index.html.erb +25 -0
  43. data/test/dummy/app/views/quo_vadis/sessions/index.html.erb +26 -0
  44. data/test/dummy/app/views/quo_vadis/sessions/new.html.erb +24 -0
  45. data/test/dummy/app/views/quo_vadis/totps/challenge.html.erb +11 -0
  46. data/test/dummy/app/views/quo_vadis/totps/new.html.erb +17 -0
  47. data/test/dummy/app/views/quo_vadis/twofas/show.html.erb +20 -0
  48. data/test/dummy/app/views/sign_ups/new.html.erb +37 -0
  49. data/test/dummy/app/views/sign_ups/show.html.erb +5 -0
  50. data/test/dummy/app/views/users/new.html.erb +37 -0
  51. data/test/dummy/config.ru +7 -0
  52. data/test/dummy/config/application.rb +30 -0
  53. data/test/dummy/config/boot.rb +4 -0
  54. data/test/dummy/config/database.yml +10 -0
  55. data/test/dummy/config/environment.rb +4 -0
  56. data/test/dummy/config/initializers/quo_vadis.rb +7 -0
  57. data/test/dummy/config/routes.rb +13 -0
  58. data/test/dummy/db/migrate/202102121932_create_users.rb +10 -0
  59. data/test/dummy/db/migrate/202102121935_create_people.rb +10 -0
  60. data/test/dummy/db/schema.rb +92 -0
  61. data/test/dummy/public/favicon.ico +0 -0
  62. data/test/fixtures/quo_vadis/mailer/account_confirmation.text +4 -0
  63. data/test/fixtures/quo_vadis/mailer/email_change_notification.text +8 -0
  64. data/test/fixtures/quo_vadis/mailer/identifier_change_notification.text +8 -0
  65. data/test/fixtures/quo_vadis/mailer/password_change_notification.text +8 -0
  66. data/test/fixtures/quo_vadis/mailer/password_reset_notification.text +8 -0
  67. data/test/fixtures/quo_vadis/mailer/recovery_codes_generation_notification.text +8 -0
  68. data/test/fixtures/quo_vadis/mailer/reset_password.text +4 -0
  69. data/test/fixtures/quo_vadis/mailer/totp_reuse_notification.text +6 -0
  70. data/test/fixtures/quo_vadis/mailer/totp_setup_notification.text +8 -0
  71. data/test/fixtures/quo_vadis/mailer/twofa_deactivated_notification.text +8 -0
  72. data/test/integration/account_confirmation_test.rb +112 -0
  73. data/test/integration/controller_test.rb +280 -0
  74. data/test/integration/logging_test.rb +235 -0
  75. data/test/integration/password_change_test.rb +93 -0
  76. data/test/integration/password_login_test.rb +125 -0
  77. data/test/integration/password_reset_test.rb +136 -0
  78. data/test/integration/recovery_codes_test.rb +48 -0
  79. data/test/integration/sessions_test.rb +86 -0
  80. data/test/integration/sign_up_test.rb +35 -0
  81. data/test/integration/totps_test.rb +96 -0
  82. data/test/integration/twofa_test.rb +82 -0
  83. data/test/mailers/mailer_test.rb +200 -0
  84. data/test/models/account_test.rb +34 -0
  85. data/test/models/crypt_test.rb +22 -0
  86. data/test/models/log_test.rb +16 -0
  87. data/test/models/mask_ip_test.rb +27 -0
  88. data/test/models/model_test.rb +66 -0
  89. data/test/models/password_test.rb +163 -0
  90. data/test/models/recovery_code_test.rb +54 -0
  91. data/test/models/session_test.rb +67 -0
  92. data/test/models/token_test.rb +70 -0
  93. data/test/models/totp_test.rb +68 -0
  94. data/test/quo_vadis_test.rb +43 -0
  95. data/test/test_helper.rb +58 -0
  96. metadata +119 -4
  97. data/Gemfile.lock +0 -178
@@ -0,0 +1,8 @@
1
+ Two-factor authentication was deactivated on your account just now.
2
+
3
+ Location: <%= @ip %>
4
+ Time: <%= @timestamp.strftime '%e %B at %H:%M (%Z)' %>
5
+
6
+ If this was you, you don't need to do anything.
7
+
8
+ If this wasn't you, please let us know.
@@ -0,0 +1,25 @@
1
+ <h1>Reset password</h1>
2
+
3
+ <% if @password.errors.any? %>
4
+ <ul>
5
+ <% @password.errors.full_messages.each do |msg| %>
6
+ <li><%= msg %></li>
7
+ <% end %>
8
+ </ul>
9
+ <% end %>
10
+
11
+ <%= form_with url: password_reset_path(params[:token]), method: :put do |f| %>
12
+ <p>
13
+ <%= f.label :password %>
14
+ <%= f.password_field :password, autocomplete: 'new-password' %>
15
+ </p>
16
+
17
+ <p>
18
+ <%= f.label :password_confirmation %>
19
+ <%= f.password_field :password_confirmation, autocomplete: 'new-password' %>
20
+ </p>
21
+
22
+ <p>
23
+ <%= f.submit %>
24
+ </p>
25
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <h1>Password reset</h1>
2
+
3
+ <p>Please check your email.</p>
4
+
5
+ <p><%= link_to 'Request a new email', new_password_reset_path %></p>
@@ -0,0 +1,12 @@
1
+ <h1>Reset password</h1>
2
+
3
+ <%= form_with url: password_resets_path, method: :post do |f| %>
4
+ <p>
5
+ <%= f.label :email %>
6
+ <%= f.text_field :email, inputmode: 'email', autocomplete: 'email' %>
7
+ </p>
8
+
9
+ <p>
10
+ <%= f.submit %>
11
+ </p>
12
+ <% end %>
@@ -0,0 +1,30 @@
1
+ <h1>Change password</h1>
2
+
3
+ <% if @password.errors.any? %>
4
+ <ul>
5
+ <% @password.errors.full_messages.each do |msg| %>
6
+ <li><%= msg %></li>
7
+ <% end %>
8
+ </ul>
9
+ <% end %>
10
+
11
+ <%= form_with url: password_path, method: :put do |f| %>
12
+ <p>
13
+ <%= f.label :password %>
14
+ <%= f.password_field :password, autocomplete: 'current-password' %>
15
+ </p>
16
+
17
+ <p>
18
+ <%= f.label :new_password %>
19
+ <%= f.password_field :new_password, autocomplete: 'new-password' %>
20
+ </p>
21
+
22
+ <p>
23
+ <%= f.label :new_password_confirmation %>
24
+ <%= f.password_field :new_password_confirmation, autocomplete: 'new-password' %>
25
+ </p>
26
+
27
+ <p>
28
+ <%= f.submit %>
29
+ </p>
30
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <h1>2FA: Recovery code</h1>
2
+
3
+ <p>Enter one of your recovery codes:</p>
4
+
5
+ <%= form_with url: authenticate_recovery_codes_path, method: :post do |f| %>
6
+ <%= f.text_field :code, inputmode: 'decimal', autofocus: true, maxlength: '11' %>
7
+
8
+ <%= f.submit 'Verify' %>
9
+ <% end %>
10
+
11
+ <p><%= link_to 'Never mind, I want to use my TOTP verification code!', quo_vadis.challenge_totps_path %></p>
@@ -0,0 +1,25 @@
1
+ <h1>2FA: Recovery codes</h1>
2
+
3
+ <p>If you lose your authenticator app, you can use recovery codes instead while you set up a new authenticator app.</p>
4
+
5
+ <p>Each code can only be used once.</p>
6
+
7
+ <% if @codes.present? %>
8
+
9
+ <p>This is the only time these codes can be shown to you! We recommend you print them out and store them somewhere safely – but not next to your 2FA details in your authenticator app.</p>
10
+
11
+ <ul>
12
+ <% @codes.each do |code| %>
13
+ <li><tt><%= code %></tt></li>
14
+ <% end %>
15
+ </ul>
16
+
17
+ <% else %>
18
+
19
+ <p>You have <%= pluralize @recovery_code_count, 'recovery code' %> left.</p>
20
+
21
+ <% end %>
22
+
23
+ <p>You can generate a fresh set of recovery codes whenever you like.</p>
24
+
25
+ <p><%= button_to 'Generate a fresh set of recovery codes', generate_recovery_codes_path %></p>
@@ -0,0 +1,26 @@
1
+ <h1>Sessions</h1>
2
+
3
+ <table>
4
+ <thead>
5
+ <tr>
6
+ <th>IP</th>
7
+ <th>User agent</th>
8
+ <th></th>
9
+ </tr>
10
+ </thead>
11
+ <tbody>
12
+ <% @qv_sessions.each do |sess| %>
13
+ <tr>
14
+ <td><%= sess.ip %></td>
15
+ <td><%= sess.user_agent %></td>
16
+ <td>
17
+ <% if sess.id == @qv_session.id %>
18
+ This session
19
+ <% else %>
20
+ <%= button_to 'Log out', quo_vadis.session_path(sess), method: :delete %>
21
+ <% end %>
22
+ </td>
23
+ </tr>
24
+ <% end %>
25
+ </tbody>
26
+ </table>
@@ -0,0 +1,24 @@
1
+ <h1>Login</h1>
2
+
3
+ <%= form_with url: login_path, method: :post do |f| %>
4
+ <p>
5
+ <%= f.label :email %>
6
+ <%= f.text_field :email, inputmode: 'email', autocomplete: 'email' %>
7
+ </p>
8
+
9
+ <p>
10
+ <%= f.label :password %>
11
+ <%= f.password_field :password, autocomplete: 'current-password' %>
12
+ </p>
13
+
14
+ <p>
15
+ <%= f.label :remember %>
16
+ <%= f.check_box :remember %>
17
+ </p>
18
+
19
+ <p>
20
+ <%= f.submit %>
21
+ </p>
22
+ <% end %>
23
+
24
+ <%= link_to 'I forgot my password', new_password_reset_path %>
@@ -0,0 +1,11 @@
1
+ <h1>2FA</h1>
2
+
3
+ <p>Enter the 6 digit code generated by your authenticator:</p>
4
+
5
+ <%= form_with url: authenticate_totps_path, method: :post do |f| %>
6
+ <%= f.text_field :totp, inputmode: 'decimal', autocomplete: 'one-time-code', autofocus: true, maxlength: '6' %>
7
+
8
+ <%= f.submit 'Verify' %>
9
+ <% end %>
10
+
11
+ <p><%= link_to 'I want to use a recovery code instead.', quo_vadis.challenge_recovery_codes_path %></p>
@@ -0,0 +1,17 @@
1
+ <h1>2FA: TOTP setup</h1>
2
+
3
+ <p>1. With your authenticator app, either scan this QR code or enter the text: <pre><%= @totp.key %></pre></p>
4
+
5
+ <%== @totp.qr_code.as_svg module_size: 5 %>
6
+
7
+
8
+ <p>2. Enter the 6 digit code generated by your authenticator, to check it worked:</p>
9
+
10
+ <%= form_with model: @totp do |f| %>
11
+ <%= f.hidden_field :key %>
12
+ <%= f.hidden_field :hmac_key %>
13
+
14
+ <%= f.text_field :otp, inputmode: 'decimal', autocomplete: 'one-time-code', autofocus: true, maxlength: '6' %>
15
+
16
+ <%= f.submit 'Confirm' %>
17
+ <% end %>
@@ -0,0 +1,20 @@
1
+ <h1>2FA</h1>
2
+
3
+ <% if authenticated_model.qv_account.has_two_factors? %>
4
+
5
+ <p>You have set up 2FA.</p>
6
+ <p><%= button_to 'Deactivate your 2FA credentials', twofa_path, method: :delete %></p>
7
+
8
+ <% else %>
9
+
10
+ <p>You do not have 2FA set up.</p>
11
+ <% if QuoVadis.two_factor_authentication_mandatory %>
12
+ <p>Please <%= link_to 'set it up', new_totp_path %> now.</p>
13
+ <% else %>
14
+ <p>You can <%= link_to 'set it up', new_totp_path %> if you like.</p>
15
+ <% end %>
16
+
17
+ <% end %>
18
+
19
+
20
+ <p> You have <%= link_to pluralize(@recovery_codes_count, 'recovery code'), recovery_codes_path %> left.</p>
@@ -0,0 +1,37 @@
1
+ <h1>New sign up (with confirmation)</h1>
2
+
3
+
4
+ <% if @user.errors.any? %>
5
+ <ul>
6
+ <% @user.errors.full_messages.each do |message| %>
7
+ <li><%= message %></li>
8
+ <% end %>
9
+ </ul>
10
+ <% end %>
11
+
12
+
13
+ <%= form_with model: @user, url: sign_ups_path do |f| %>
14
+ <p>
15
+ <%= f.label :name %>
16
+ <%= f.text_field :name %>
17
+ </p>
18
+
19
+ <p>
20
+ <%= f.label :email %>
21
+ <%= f.text_field :email %>
22
+ </p>
23
+
24
+ <p>
25
+ <%= f.label :password %>
26
+ <%= f.password_field :password %>
27
+ </p>
28
+
29
+ <p>
30
+ <%= f.label :password_confirmation %>
31
+ <%= f.password_field :password_confirmation %>
32
+ </p>
33
+
34
+ <p>
35
+ <%= f.submit %>
36
+ </p>
37
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <h1>User</h1>
2
+
3
+ <p><%= @user.name %></p>
4
+
5
+ <p><%= @user.email %></p>
@@ -0,0 +1,37 @@
1
+ <h1>New user (without confirmation)</h1>
2
+
3
+
4
+ <% if @user.errors.any? %>
5
+ <ul>
6
+ <% @user.errors.full_messages.each do |message| %>
7
+ <li><%= message %></li>
8
+ <% end %>
9
+ </ul>
10
+ <% end %>
11
+
12
+
13
+ <%= form_with model: @user do |f| %>
14
+ <p>
15
+ <%= f.label :name %>
16
+ <%= f.text_field :name %>
17
+ </p>
18
+
19
+ <p>
20
+ <%= f.label :email %>
21
+ <%= f.text_field :email %>
22
+ </p>
23
+
24
+ <p>
25
+ <%= f.label :password %>
26
+ <%= f.password_field :password %>
27
+ </p>
28
+
29
+ <p>
30
+ <%= f.label :password_confirmation %>
31
+ <%= f.password_field :password_confirmation %>
32
+ </p>
33
+
34
+ <p>
35
+ <%= f.submit %>
36
+ </p>
37
+ <% end %>
@@ -0,0 +1,7 @@
1
+ require_relative 'config/environment'
2
+
3
+ run Rails.application
4
+ Rails.application.load_server
5
+
6
+
7
+ # copied from qvfull - not sure if necessary
@@ -0,0 +1,30 @@
1
+ require_relative 'boot'
2
+
3
+ # copied from https://github.com/janko/rodauth-rails/blob/master/test/rails_app/config/application.rb
4
+
5
+ # require "rails/all"
6
+ require "active_model/railtie"
7
+ require "active_record/railtie"
8
+ # require "action_controller/railtie"
9
+ # require "action_view/railtie"
10
+ require "action_mailer/railtie"
11
+ require "rails/test_unit/railtie"
12
+
13
+ Bundler.require(*Rails.groups)
14
+
15
+ require 'quo_vadis'
16
+
17
+ module Dummy
18
+ class Application < Rails::Application
19
+ config.load_defaults Rails::VERSION::STRING.to_f
20
+
21
+ # config.logger = Logger.new nil
22
+ config.eager_load = true
23
+ config.action_dispatch.show_exceptions = false
24
+ config.action_controller.allow_forgery_protection = false
25
+
26
+ config.action_mailer.delivery_method = :test
27
+ config.action_mailer.default_url_options = { host: 'example.com' }
28
+ config.action_mailer.delivery_job = 'ActionMailer::MailDeliveryJob'
29
+ end
30
+ end
@@ -0,0 +1,4 @@
1
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path('../../../Gemfile', __dir__)
2
+
3
+ require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
4
+ $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)
@@ -0,0 +1,10 @@
1
+ development:
2
+ adapter: sqlite3
3
+ pool: 5
4
+ timeout: 5000
5
+ database: db/development.sqlite3
6
+ test:
7
+ adapter: sqlite3
8
+ pool: 5
9
+ timeout: 5000
10
+ database: db/test.sqlite3
@@ -0,0 +1,4 @@
1
+ require_relative 'application'
2
+
3
+ Dummy::Application.initialize!
4
+ # Rails.application.initialize!
@@ -0,0 +1,7 @@
1
+ QuoVadis.configure do
2
+ # Cannot use the __Host- prefix in a non-SSL environment.
3
+ # https://tools.ietf.org/html/draft-west-cookie-prefixes-05#section-3.2
4
+ cookie_name 'qv'
5
+
6
+ session_lifetime 1.week
7
+ end
@@ -0,0 +1,13 @@
1
+ Rails.application.routes.draw do
2
+ resources :users
3
+ resources :sign_ups
4
+ resources :articles do
5
+ collection do
6
+ get 'secret'
7
+ get 'also_secret'
8
+ get 'very_secret'
9
+ end
10
+ end
11
+ get '/articles/secret', as: 'after_login'
12
+ root 'articles#index'
13
+ end
@@ -0,0 +1,10 @@
1
+ class CreateUsers < ActiveRecord::Migration[6.1]
2
+ def change
3
+ create_table :users do |t|
4
+ t.string :name, null: false
5
+ t.string :email, null: false
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ class CreatePeople < ActiveRecord::Migration[6.1]
2
+ def change
3
+ create_table :people do |t|
4
+ t.string :username, null: false
5
+ t.string :email, null: false
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,92 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # This file is the source Rails uses to define your schema when running `bin/rails
6
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7
+ # be faster and is potentially less error prone than running all of your
8
+ # migrations from scratch. Old migrations may fail to apply correctly if those
9
+ # migrations use external dependencies or application code.
10
+ #
11
+ # It's strongly recommended that you check this file into your version control system.
12
+
13
+ ActiveRecord::Schema.define(version: 202102150904) do
14
+
15
+ create_table "people", force: :cascade do |t|
16
+ t.string "username", null: false
17
+ t.string "email", null: false
18
+ t.datetime "created_at", precision: 6, null: false
19
+ t.datetime "updated_at", precision: 6, null: false
20
+ end
21
+
22
+ create_table "qv_accounts", force: :cascade do |t|
23
+ t.string "model_type", null: false
24
+ t.bigint "model_id", null: false
25
+ t.string "identifier", null: false
26
+ t.datetime "confirmed_at"
27
+ t.datetime "created_at", precision: 6, null: false
28
+ t.datetime "updated_at", precision: 6, null: false
29
+ t.index ["identifier"], name: "index_qv_accounts_on_identifier", unique: true
30
+ t.index ["model_type", "model_id"], name: "index_qv_accounts_on_model_type_and_model_id", unique: true
31
+ end
32
+
33
+ create_table "qv_logs", force: :cascade do |t|
34
+ t.bigint "account_id"
35
+ t.string "action", null: false
36
+ t.string "ip", null: false
37
+ t.json "metadata", default: {}, null: false
38
+ t.datetime "created_at", precision: 6, null: false
39
+ t.datetime "updated_at", precision: 6, null: false
40
+ t.index ["account_id"], name: "index_qv_logs_on_account_id"
41
+ end
42
+
43
+ create_table "qv_passwords", force: :cascade do |t|
44
+ t.bigint "account_id", null: false
45
+ t.string "password_digest", null: false
46
+ t.datetime "created_at", precision: 6, null: false
47
+ t.datetime "updated_at", precision: 6, null: false
48
+ t.index ["account_id"], name: "index_qv_passwords_on_account_id"
49
+ end
50
+
51
+ create_table "qv_recovery_codes", force: :cascade do |t|
52
+ t.bigint "account_id", null: false
53
+ t.string "code_digest", null: false
54
+ t.datetime "created_at", precision: 6, null: false
55
+ t.datetime "updated_at", precision: 6, null: false
56
+ t.index ["account_id"], name: "index_qv_recovery_codes_on_account_id"
57
+ end
58
+
59
+ create_table "qv_sessions", force: :cascade do |t|
60
+ t.bigint "account_id", null: false
61
+ t.string "ip", null: false
62
+ t.string "user_agent", null: false
63
+ t.datetime "lifetime_expires_at"
64
+ t.datetime "last_seen_at"
65
+ t.datetime "second_factor_at"
66
+ t.datetime "created_at", precision: 6, null: false
67
+ t.datetime "updated_at", precision: 6, null: false
68
+ t.index ["account_id"], name: "index_qv_sessions_on_account_id"
69
+ end
70
+
71
+ create_table "qv_totps", force: :cascade do |t|
72
+ t.bigint "account_id", null: false
73
+ t.string "key", null: false
74
+ t.integer "last_used_at", null: false
75
+ t.datetime "created_at", precision: 6, null: false
76
+ t.datetime "updated_at", precision: 6, null: false
77
+ t.index ["account_id"], name: "index_qv_totps_on_account_id"
78
+ end
79
+
80
+ create_table "users", force: :cascade do |t|
81
+ t.string "name", null: false
82
+ t.string "email", null: false
83
+ t.datetime "created_at", precision: 6, null: false
84
+ t.datetime "updated_at", precision: 6, null: false
85
+ end
86
+
87
+ add_foreign_key "qv_logs", "qv_accounts", column: "account_id"
88
+ add_foreign_key "qv_passwords", "qv_accounts", column: "account_id"
89
+ add_foreign_key "qv_recovery_codes", "qv_accounts", column: "account_id"
90
+ add_foreign_key "qv_sessions", "qv_accounts", column: "account_id"
91
+ add_foreign_key "qv_totps", "qv_accounts", column: "account_id"
92
+ end