katalyst-koi 4.14.2 → 4.15.0
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.
- checksums.yaml +4 -4
- data/app/controllers/admin/admin_users_controller.rb +2 -0
- data/app/controllers/admin/otps_controller.rb +52 -0
- data/app/controllers/admin/sessions_controller.rb +67 -12
- data/app/models/admin/user.rb +22 -0
- data/app/models/concerns/koi/model/otp.rb +21 -0
- data/app/views/admin/admin_users/index.html.erb +3 -0
- data/app/views/admin/admin_users/show.html+self.erb +16 -2
- data/app/views/admin/admin_users/show.html.erb +4 -6
- data/app/views/admin/otps/_form.html.erb +31 -0
- data/app/views/admin/otps/create.turbo_stream.erb +5 -0
- data/app/views/admin/otps/new.html.erb +5 -0
- data/app/views/admin/sessions/new.html.erb +10 -10
- data/app/views/admin/sessions/otp.html.erb +10 -0
- data/app/views/admin/sessions/password.html.erb +11 -0
- data/config/initializers/inflections.rb +6 -0
- data/config/locales/koi.en.yml +5 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20241214060913_add_otp_secret_to_admin_users.rb +7 -0
- data/lib/koi/engine.rb +2 -0
- data/lib/koi/form/elements/file_element.rb +1 -1
- data/spec/factories/admins.rb +1 -0
- metadata +39 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 727d460171f01afa0e58e2640f5a42b1f23262f9b18d03a8eeeb9fbc2e54c8ea
|
4
|
+
data.tar.gz: db82baad11c408b11cbd1842632acca9d72b476180fef1af70871dadba4f2bfa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc0df96b3f59832d975518959f57500a68af1535e051365d75f9943e06e13aedc30b7d00c25817ecad213486c560cb71bcc64b6ba6c57f395bb3f81ef46deca5
|
7
|
+
data.tar.gz: 19477da2a8da96c68947a0d6d4903aeece46e91aed0902fa29e3a3abf87c4f4699254a42eb988f623c92e2f3be042995ea1d789c9bff7edb87f7767a8a964c46
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Admin
|
4
|
+
class OtpsController < ApplicationController
|
5
|
+
before_action :set_admin_user
|
6
|
+
|
7
|
+
def new
|
8
|
+
@admin_user.otp_secret = ROTP::Base32.random
|
9
|
+
|
10
|
+
render :new, locals: { admin: @admin_user }
|
11
|
+
end
|
12
|
+
|
13
|
+
def create
|
14
|
+
@admin_user.otp_secret = otp_params[:otp_secret]
|
15
|
+
|
16
|
+
if @admin_user.otp.verify(otp_params[:token])
|
17
|
+
@admin_user.save
|
18
|
+
|
19
|
+
redirect_to admin_admin_user_path(@admin_user), status: :see_other
|
20
|
+
else
|
21
|
+
@admin_user.errors.add(:token, :invalid)
|
22
|
+
|
23
|
+
respond_to do |format|
|
24
|
+
format.html { redirect_to admin_admin_user_path(@admin_user), status: :see_other }
|
25
|
+
format.turbo_stream { render locals: { admin: @admin_user } }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def destroy
|
31
|
+
@admin_user.update!(otp_secret: nil)
|
32
|
+
|
33
|
+
redirect_to admin_admin_user_path(@admin_user), status: :see_other
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def otp_params
|
39
|
+
params.require(:admin).permit(:otp_secret, :token)
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_admin_user
|
43
|
+
@admin_user = Admin::User.find(params[:admin_user_id])
|
44
|
+
|
45
|
+
if current_admin == @admin_user
|
46
|
+
request.variant = :self
|
47
|
+
else
|
48
|
+
head(:forbidden)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -14,15 +14,20 @@ module Admin
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def create
|
17
|
-
if
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
if session_params[:response].present?
|
18
|
+
create_session_with_webauthn
|
19
|
+
elsif session_params[:token].present?
|
20
|
+
create_session_with_token
|
21
|
+
elsif session_params[:password].present?
|
22
|
+
create_session_with_password
|
23
|
+
elsif session_params[:email].present?
|
24
|
+
# conversational flow, ask for password regardless of email
|
25
|
+
admin_user = Admin::User.new(session_params.slice(:email))
|
26
|
+
|
27
|
+
render(:password, status: :unprocessable_content, locals: { admin_user: })
|
23
28
|
else
|
24
|
-
|
25
|
-
admin_user
|
29
|
+
# invalid request, re-render new
|
30
|
+
admin_user = Admin::User.new
|
26
31
|
|
27
32
|
render(:new, status: :unprocessable_content, locals: { admin_user: })
|
28
33
|
end
|
@@ -38,16 +43,66 @@ module Admin
|
|
38
43
|
|
39
44
|
private
|
40
45
|
|
46
|
+
def create_session_with_password
|
47
|
+
# constant time lookup for user with password verification
|
48
|
+
admin_user = Admin::User.authenticate_by(session_params.slice(:email, :password))
|
49
|
+
|
50
|
+
if admin_user.present? && admin_user.requires_otp?
|
51
|
+
session[:pending_admin_user_id] = admin_user.id
|
52
|
+
|
53
|
+
render(:otp, status: :unprocessable_content, locals: { admin_user: })
|
54
|
+
elsif admin_user.present?
|
55
|
+
admin_sign_in(admin_user)
|
56
|
+
else
|
57
|
+
admin_user = Admin::User.new(session_params.slice(:email, :password))
|
58
|
+
admin_user.errors.add(:email, :invalid)
|
59
|
+
|
60
|
+
render(:new, status: :unprocessable_content, locals: { admin_user: })
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def create_session_with_token
|
65
|
+
# assume that the previous step injected the user's ID into the session and remove it regardless of outcome
|
66
|
+
admin_user = Admin::User.find_by(id: session.delete(:pending_admin_user_id))
|
67
|
+
|
68
|
+
if admin_user&.otp&.verify(session_params[:token],
|
69
|
+
drift_ahead: 15,
|
70
|
+
drift_behind: 15,
|
71
|
+
after: admin_user.current_sign_in_at)
|
72
|
+
admin_sign_in(admin_user)
|
73
|
+
else
|
74
|
+
admin_user = Admin::User.new
|
75
|
+
admin_user.errors.add(:email, :invalid)
|
76
|
+
|
77
|
+
render(:new, status: :unprocessable_content, locals: { admin_user: })
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def create_session_with_webauthn
|
82
|
+
if (admin_user = webauthn_authenticate!)
|
83
|
+
admin_sign_in(admin_user)
|
84
|
+
else
|
85
|
+
admin_user = Admin::User.new
|
86
|
+
admin_user.errors.add(:email, :invalid)
|
87
|
+
|
88
|
+
render(:new, status: :unprocessable_content, locals: { admin_user: })
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
41
92
|
def redirect_authenticated
|
42
93
|
redirect_to(admin_dashboard_path, status: :see_other)
|
43
94
|
end
|
44
95
|
|
45
|
-
def
|
46
|
-
|
96
|
+
def admin_sign_in(admin_user)
|
97
|
+
record_sign_in!(admin_user)
|
98
|
+
|
99
|
+
session[:admin_user_id] = admin_user.id
|
100
|
+
|
101
|
+
redirect_to(url_from(params[:redirect].presence) || admin_dashboard_path, status: :see_other)
|
47
102
|
end
|
48
103
|
|
49
|
-
def
|
50
|
-
|
104
|
+
def session_params
|
105
|
+
params.require(:admin).permit(:email, :password, :token, :response)
|
51
106
|
end
|
52
107
|
|
53
108
|
def update_last_sign_in(admin_user)
|
data/app/models/admin/user.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
module Admin
|
4
4
|
class User < ApplicationRecord
|
5
5
|
include Koi::Model::Archivable
|
6
|
+
include Koi::Model::OTP
|
6
7
|
|
7
8
|
def self.model_name
|
8
9
|
ActiveModel::Name.new(self, nil, "Admin")
|
@@ -27,5 +28,26 @@ module Admin
|
|
27
28
|
where("email LIKE :query OR name LIKE :query", query: "%#{query}%")
|
28
29
|
end
|
29
30
|
end
|
31
|
+
|
32
|
+
scope :has_otp, ->(otp) do
|
33
|
+
if otp
|
34
|
+
where.not(otp_secret: nil)
|
35
|
+
else
|
36
|
+
where(otp_secret: nil)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
scope :has_passkey, ->(passkey) do
|
41
|
+
if passkey
|
42
|
+
where(id: Admin::Credential.select(:admin_id))
|
43
|
+
else
|
44
|
+
where.not(id: Admin::Credential.select(:admin_id))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def passkey
|
49
|
+
credentials.any?
|
50
|
+
end
|
51
|
+
alias passkey? passkey
|
30
52
|
end
|
31
53
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koi
|
4
|
+
module Model
|
5
|
+
module OTP
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
attribute :token, :string
|
10
|
+
end
|
11
|
+
|
12
|
+
def requires_otp?
|
13
|
+
otp_secret.present?
|
14
|
+
end
|
15
|
+
|
16
|
+
def otp
|
17
|
+
ROTP::TOTP.new(otp_secret) if otp_secret.present?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -18,6 +18,9 @@
|
|
18
18
|
<% row.boolean :credentials, label: "Passkey" do |cell| %>
|
19
19
|
<%= cell.value.any? ? "Yes" : "No" %>
|
20
20
|
<% end %>
|
21
|
+
<% row.boolean :otp, label: "MFA" do |cell| %>
|
22
|
+
<%= cell.value.present? ? "Yes" : "No" %>
|
23
|
+
<% end %>
|
21
24
|
<% end %>
|
22
25
|
|
23
26
|
<%= table_pagination_with(collection:) %>
|
@@ -4,11 +4,25 @@
|
|
4
4
|
<%= render Koi::Header::ShowComponent.new(resource: admin) %>
|
5
5
|
<% end %>
|
6
6
|
|
7
|
-
<%= render Koi::
|
7
|
+
<%= render Koi::SummaryTableComponent.new(model: admin) do |builder| %>
|
8
8
|
<%= builder.text :name %>
|
9
9
|
<%= builder.text :email %>
|
10
10
|
<%= builder.date :created_at %>
|
11
|
-
<%= builder.date :last_sign_in_at, label:
|
11
|
+
<%= builder.date :last_sign_in_at, label: "Last sign in" %>
|
12
|
+
<%= builder.boolean :passkey %>
|
13
|
+
<%= builder.boolean :otp, label: "MFA" do |otp| %>
|
14
|
+
<span class="repel">
|
15
|
+
<%= otp %>
|
16
|
+
<% if otp.value %>
|
17
|
+
<%= button_to("Remove", admin_admin_user_otp_path(admin),
|
18
|
+
class: "button button--text",
|
19
|
+
method: :delete,
|
20
|
+
form: { data: { turbo_confirm: "Are you sure?" } }) %>
|
21
|
+
<% else %>
|
22
|
+
<%= kpop_link_to "Add", new_admin_admin_user_otp_path(admin) %>
|
23
|
+
<% end %>
|
24
|
+
</span>
|
25
|
+
<% end %>
|
12
26
|
<% end %>
|
13
27
|
|
14
28
|
<div class="repel">
|
@@ -4,18 +4,16 @@
|
|
4
4
|
<%= render Koi::Header::ShowComponent.new(resource: admin) %>
|
5
5
|
<% end %>
|
6
6
|
|
7
|
-
<%= render Koi::
|
7
|
+
<%= render Koi::SummaryTableComponent.new(model: admin, class: "item-table") do |builder| %>
|
8
8
|
<%= builder.text :name %>
|
9
9
|
<%= builder.text :email %>
|
10
10
|
<%= builder.date :created_at %>
|
11
|
-
<%= builder.date :last_sign_in_at, label:
|
11
|
+
<%= builder.date :last_sign_in_at, label: "Last sign in" %>
|
12
|
+
<%= builder.boolean :passkey %>
|
13
|
+
<%= builder.boolean :otp, label: "MFA" %>
|
12
14
|
<%= builder.boolean :archived? %>
|
13
15
|
<% end %>
|
14
16
|
|
15
|
-
<h3>Passkeys</h3>
|
16
|
-
|
17
|
-
<%= render "admin/credentials/credentials", admin: %>
|
18
|
-
|
19
17
|
<div class="actions">
|
20
18
|
<% if admin.archived? %>
|
21
19
|
<%= button_to "Delete", admin_admin_user_path(admin),
|
@@ -0,0 +1,31 @@
|
|
1
|
+
<%# locals: (admin:) %>
|
2
|
+
|
3
|
+
<%= form_with(id: dom_id(admin, :otp),
|
4
|
+
model: admin,
|
5
|
+
url: admin_admin_user_otp_path(admin),
|
6
|
+
method: :post,
|
7
|
+
class: "flow") do |form| %>
|
8
|
+
<section class="flow prose">
|
9
|
+
<p>MFA protects your account by requiring you to enter a six-digit
|
10
|
+
token that changes every 30 seconds. If someone knows or guesses your
|
11
|
+
password they also need to know the current token to log in.</p>
|
12
|
+
<p>In general, we recommend using Passkeys over MFA. Passkeys offer better
|
13
|
+
security than a password + MFA, and they are easier to use.</p>
|
14
|
+
<p><strong>Add an MFA authenticator to your account</strong></p>
|
15
|
+
<ol class="flow">
|
16
|
+
<li>Install an MFA app. Most password managers support MFA.</li>
|
17
|
+
<li>Scan this code using your mobile device or password manager:<br>
|
18
|
+
<%== RQRCode::QRCode.new(admin.otp.provisioning_uri(admin.email)).as_svg(
|
19
|
+
color: "000",
|
20
|
+
shape_rendering: "crispEdges",
|
21
|
+
module_size: 3,
|
22
|
+
use_path: true,
|
23
|
+
) %>
|
24
|
+
</li>
|
25
|
+
<li>Enter the token shown in your app into the field below:</li>
|
26
|
+
</ol>
|
27
|
+
</section>
|
28
|
+
<%= form.hidden_field :otp_secret %>
|
29
|
+
<%= form.govuk_text_field :token %>
|
30
|
+
<%= form.admin_save %>
|
31
|
+
<% end %>
|
@@ -1,4 +1,7 @@
|
|
1
|
+
<%# locals: (admin_user:) %>
|
2
|
+
|
1
3
|
<%= render "layouts/koi/navigation_header" %>
|
4
|
+
|
2
5
|
<%= form_with(
|
3
6
|
model: admin_user,
|
4
7
|
url: admin_session_path,
|
@@ -6,7 +9,7 @@
|
|
6
9
|
controller: "webauthn-authentication",
|
7
10
|
webauthn_authentication_options_value: { publicKey: webauthn_auth_options },
|
8
11
|
},
|
9
|
-
) do |
|
12
|
+
) do |form| %>
|
10
13
|
<% (redirect = flash[:redirect] || params[:redirect]) && flash.delete(:redirect) %>
|
11
14
|
<% unless flash.empty? %>
|
12
15
|
<div class="govuk-error-summary">
|
@@ -17,15 +20,12 @@
|
|
17
20
|
</ul>
|
18
21
|
</div>
|
19
22
|
<% end %>
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
<%= f.hidden_field :response, data: { webauthn_authentication_target: "response" } %>
|
25
|
-
<%= hidden_field_tag(:redirect, redirect) %>
|
26
|
-
<% end %>
|
23
|
+
<%# note, autocomplete off is ignored by browsers but required by PCI-DSS %>
|
24
|
+
<%= form.govuk_email_field :email, autofocus: true, autocomplete: "off" %>
|
25
|
+
<%= form.hidden_field :response, data: { webauthn_authentication_target: "response" } %>
|
26
|
+
<%= hidden_field_tag(:redirect, redirect) %>
|
27
27
|
<div class="actions-group">
|
28
|
-
<%=
|
29
|
-
<%=
|
28
|
+
<%= form.admin_save "Next" %>
|
29
|
+
<%= form.button "🔑", type: :button, class: "button button--secondary", data: { action: "webauthn-authentication#authenticate" } %>
|
30
30
|
</div>
|
31
31
|
<% end %>
|
@@ -0,0 +1,10 @@
|
|
1
|
+
<%# locals: (admin_user:) %>
|
2
|
+
|
3
|
+
<%= render "layouts/koi/navigation_header" %>
|
4
|
+
|
5
|
+
<%= form_with(model: admin_user, url: admin_session_path, method: :post) do |form| %>
|
6
|
+
<%# note, autocomplete off is ignored by browsers but required by PCI-DSS %>
|
7
|
+
<%= form.govuk_text_field :token, autofocus: true, autocomplete: "off" %>
|
8
|
+
<%= hidden_field_tag(:redirect, params[:redirect]) %>
|
9
|
+
<%= form.admin_save "Next" %>
|
10
|
+
<% end %>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<%# locals: (admin_user:) %>
|
2
|
+
|
3
|
+
<%= render "layouts/koi/navigation_header" %>
|
4
|
+
|
5
|
+
<%= form_with(model: admin_user, url: admin_session_path) do |form| %>
|
6
|
+
<%= form.hidden_field(:email) %>
|
7
|
+
<%# note, autocomplete off is ignored by browsers but required by PCI-DSS %>
|
8
|
+
<%= form.govuk_password_field :password, autofocus: true, autocomplete: "off" %>
|
9
|
+
<%= hidden_field_tag(:redirect, params[:redirect]) %>
|
10
|
+
<%= form.admin_save "Next" %>
|
11
|
+
<% end %>
|
data/config/locales/koi.en.yml
CHANGED
data/config/routes.rb
CHANGED
@@ -10,6 +10,7 @@ Rails.application.routes.draw do
|
|
10
10
|
resources :url_rewrites
|
11
11
|
resources :admin_users do
|
12
12
|
resources :credentials, only: %i[new create destroy]
|
13
|
+
resource :otp, only: %i[new create destroy]
|
13
14
|
resources :tokens, only: %i[create]
|
14
15
|
get :archived, on: :collection
|
15
16
|
put :archive, on: :collection
|
data/lib/koi/engine.rb
CHANGED
data/spec/factories/admins.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: katalyst-koi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.15.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Katalyst Interactive
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-12-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -80,6 +80,34 @@ dependencies:
|
|
80
80
|
- - ">="
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rotp
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rqrcode
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
83
111
|
- !ruby/object:Gem::Dependency
|
84
112
|
name: webauthn
|
85
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -343,6 +371,7 @@ files:
|
|
343
371
|
- app/controllers/admin/caches_controller.rb
|
344
372
|
- app/controllers/admin/credentials_controller.rb
|
345
373
|
- app/controllers/admin/dashboards_controller.rb
|
374
|
+
- app/controllers/admin/otps_controller.rb
|
346
375
|
- app/controllers/admin/sessions_controller.rb
|
347
376
|
- app/controllers/admin/tokens_controller.rb
|
348
377
|
- app/controllers/admin/url_rewrites_controller.rb
|
@@ -363,6 +392,7 @@ files:
|
|
363
392
|
- app/models/admin/user.rb
|
364
393
|
- app/models/application_record.rb
|
365
394
|
- app/models/concerns/koi/model/archivable.rb
|
395
|
+
- app/models/concerns/koi/model/otp.rb
|
366
396
|
- app/models/url_rewrite.rb
|
367
397
|
- app/views/admin/admin_users/_fields.html+self.erb
|
368
398
|
- app/views/admin/admin_users/_fields.html.erb
|
@@ -378,7 +408,12 @@ files:
|
|
378
408
|
- app/views/admin/credentials/destroy.turbo_stream.erb
|
379
409
|
- app/views/admin/credentials/new.html.erb
|
380
410
|
- app/views/admin/dashboards/show.html.erb
|
411
|
+
- app/views/admin/otps/_form.html.erb
|
412
|
+
- app/views/admin/otps/create.turbo_stream.erb
|
413
|
+
- app/views/admin/otps/new.html.erb
|
381
414
|
- app/views/admin/sessions/new.html.erb
|
415
|
+
- app/views/admin/sessions/otp.html.erb
|
416
|
+
- app/views/admin/sessions/password.html.erb
|
382
417
|
- app/views/admin/shared/icons/_close.html.erb
|
383
418
|
- app/views/admin/shared/icons/_cross.html.erb
|
384
419
|
- app/views/admin/shared/icons/_menu.html.erb
|
@@ -418,6 +453,7 @@ files:
|
|
418
453
|
- config/importmap.rb
|
419
454
|
- config/initializers/extensions.rb
|
420
455
|
- config/initializers/flipper.rb
|
456
|
+
- config/initializers/inflections.rb
|
421
457
|
- config/locales/koi.en.yml
|
422
458
|
- config/locales/pagy.en.yml
|
423
459
|
- config/routes.rb
|
@@ -428,6 +464,7 @@ files:
|
|
428
464
|
- db/migrate/20230531063707_update_admin_users.rb
|
429
465
|
- db/migrate/20230602033610_add_archived_to_admin_users.rb
|
430
466
|
- db/migrate/20231211005214_add_status_code_to_url_rewrites.rb
|
467
|
+
- db/migrate/20241214060913_add_otp_secret_to_admin_users.rb
|
431
468
|
- db/seeds.rb
|
432
469
|
- lib/generators/koi/active_record/active_record_generator.rb
|
433
470
|
- lib/generators/koi/admin/USAGE
|