quo_vadis 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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