katalyst-koi 5.3.1 → 5.5.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/README.md +6 -12
- data/app/assets/builds/katalyst/koi.esm.js +9 -19
- data/app/assets/builds/katalyst/koi.js +9 -19
- data/app/assets/builds/katalyst/koi.min.js +1 -1
- data/app/assets/builds/katalyst/koi.min.js.map +1 -1
- data/app/assets/stylesheets/koi/blocks/index.css +1 -0
- data/app/assets/stylesheets/koi/{login.css → blocks/roadblock.css} +8 -5
- data/app/controllers/admin/admin_users_controller.rb +3 -2
- data/app/controllers/admin/credentials_controller.rb +33 -51
- data/app/controllers/admin/device_authorizations_controller.rb +58 -0
- data/app/controllers/admin/device_tokens_controller.rb +18 -0
- data/app/controllers/admin/otps_controller.rb +6 -16
- data/app/controllers/admin/profiles_controller.rb +33 -0
- data/app/controllers/admin/sessions_controller.rb +12 -6
- data/app/controllers/admin/tokens_controller.rb +6 -2
- data/app/controllers/concerns/koi/controller/has_admin_users.rb +8 -11
- data/app/controllers/concerns/koi/controller/has_webauthn.rb +54 -9
- data/app/controllers/concerns/koi/controller.rb +7 -0
- data/app/javascript/koi/controllers/webauthn_authentication_controller.js +1 -1
- data/app/javascript/koi/controllers/webauthn_registration_controller.js +8 -18
- data/app/jobs/admin/device_authorizations_cleanup_job.rb +9 -0
- data/app/models/admin/credential.rb +4 -0
- data/app/models/admin/device_authorization.rb +113 -0
- data/app/models/admin/user.rb +1 -0
- data/app/models/koi/current.rb +8 -0
- data/app/views/admin/admin_users/edit.html.erb +3 -1
- data/app/views/admin/admin_users/show.html.erb +3 -0
- data/app/views/admin/credentials/_credentials.html.erb +8 -5
- data/app/views/admin/credentials/new.html.erb +32 -41
- data/app/views/admin/credentials/show.html.erb +19 -0
- data/app/views/admin/device_authorizations/show.html.erb +38 -0
- data/app/views/admin/otps/_form.html.erb +1 -1
- data/app/views/admin/{admin_users/_form.html+self.erb → profiles/_form.html.erb} +1 -1
- data/app/views/admin/{admin_users/edit.html+self.erb → profiles/edit.html.erb} +0 -1
- data/app/views/admin/{admin_users/show.html+self.erb → profiles/show.html.erb} +15 -10
- data/app/views/admin/sessions/new.html.erb +26 -27
- data/app/views/admin/sessions/otp.html.erb +13 -5
- data/app/views/admin/sessions/password.html.erb +16 -8
- data/app/views/admin/tokens/show.html.erb +12 -8
- data/app/views/layouts/koi/_application_navigation.html.erb +13 -9
- data/app/views/layouts/koi/application.html.erb +19 -10
- data/config/locales/koi.en.yml +0 -1
- data/config/routes.rb +17 -9
- data/db/migrate/20260413014834_create_admin_device_authorizations.rb +20 -0
- data/lib/generators/koi/helpers/resource_helpers.rb +1 -1
- data/lib/koi/config.rb +2 -1
- data/lib/koi/engine.rb +1 -0
- data/lib/koi/middleware/admin_authentication.rb +54 -10
- data/spec/factories/admin_device_authorizations.rb +29 -0
- metadata +30 -10
- data/app/views/admin/credentials/_credentials.html+self.erb +0 -12
- data/app/views/admin/credentials/create.turbo_stream.erb +0 -5
- data/app/views/admin/credentials/destroy.turbo_stream.erb +0 -5
- data/app/views/layouts/koi/login.html.erb +0 -50
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
<%# locals: (admin_user:) %>
|
|
2
2
|
|
|
3
3
|
<% content_for(:header) do %>
|
|
4
|
-
<%= breadcrumb_list do %>
|
|
5
|
-
<li><%= link_to("Admin users", admin_admin_users_path) %></li>
|
|
6
|
-
<% end %>
|
|
7
|
-
|
|
8
4
|
<h1><%= admin_user %></h1>
|
|
9
5
|
|
|
10
6
|
<%= actions_list do %>
|
|
11
|
-
<li><%= link_to("Edit",
|
|
7
|
+
<li><%= link_to("Edit", edit_admin_profile_path) %></li>
|
|
12
8
|
<% end %>
|
|
13
9
|
<% end %>
|
|
14
10
|
|
|
@@ -22,12 +18,12 @@
|
|
|
22
18
|
<span class="repel">
|
|
23
19
|
<%= otp %>
|
|
24
20
|
<% if otp.value %>
|
|
25
|
-
<%= button_to("Remove",
|
|
21
|
+
<%= button_to("Remove", admin_profile_otp_path,
|
|
26
22
|
class: "button button--text",
|
|
27
23
|
method: :delete,
|
|
28
24
|
form: { data: { turbo_confirm: "Are you sure?" } }) %>
|
|
29
25
|
<% else %>
|
|
30
|
-
<%= link_to("Add",
|
|
26
|
+
<%= link_to("Add", new_admin_profile_otp_path,
|
|
31
27
|
class: "button", data: { turbo_frame: "edit" }) %>
|
|
32
28
|
<% end %>
|
|
33
29
|
</span>
|
|
@@ -36,10 +32,19 @@
|
|
|
36
32
|
|
|
37
33
|
<div class="repel">
|
|
38
34
|
<h3>Passkeys</h3>
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
|
|
36
|
+
<%= form_with(model: Admin::Credential.new,
|
|
37
|
+
url: admin_profile_credentials_path,
|
|
38
|
+
data: {
|
|
39
|
+
controller: "webauthn-registration",
|
|
40
|
+
action: "submit->webauthn-registration#submit",
|
|
41
|
+
webauthn_registration_options_value:,
|
|
42
|
+
}) do |form| %>
|
|
43
|
+
<%= form.hidden_field :response, data: { webauthn_registration_target: "response" } %>
|
|
44
|
+
<%= form.button("Add passkey", type: :submit, class: "button") %>
|
|
45
|
+
<% end %>
|
|
41
46
|
</div>
|
|
42
47
|
|
|
43
|
-
<%= render
|
|
48
|
+
<%= render("admin/credentials/credentials", admin_user:) %>
|
|
44
49
|
|
|
45
50
|
<%= koi_modal_tag("edit") %>
|
|
@@ -1,32 +1,31 @@
|
|
|
1
1
|
<%# locals: (admin_user:) %>
|
|
2
2
|
|
|
3
|
-
<%=
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
3
|
+
<%= content_for(:roadblock) do %>
|
|
4
|
+
<header>
|
|
5
|
+
<icon aria-hidden="true" class="icon" data-icon="koi"> </icon>
|
|
6
|
+
<h1>Koi</h1>
|
|
7
|
+
<h2><%= Koi.config.site_name || URI.parse(root_url).host %></h2>
|
|
8
|
+
</header>
|
|
9
|
+
|
|
10
|
+
<%= form_with(
|
|
11
|
+
model: admin_user,
|
|
12
|
+
scope: :admin,
|
|
13
|
+
url: admin_session_path,
|
|
14
|
+
data: {
|
|
15
|
+
controller: "webauthn-authentication",
|
|
16
|
+
webauthn_authentication_options_value:,
|
|
17
|
+
},
|
|
18
|
+
) do |form| %>
|
|
19
|
+
<%# note, autocomplete off is ignored by browsers but required by PCI-DSS %>
|
|
20
|
+
<%= form.govuk_email_field :email, autofocus: true, autocomplete: "off" %>
|
|
21
|
+
<%= form.hidden_field :response, data: { webauthn_authentication_target: "response" } %>
|
|
22
|
+
<% (redirect = flash[:redirect] || params[:redirect]) && flash.delete(:redirect) %>
|
|
23
|
+
<%= hidden_field_tag(:redirect, redirect) %>
|
|
24
|
+
<div class="actions">
|
|
25
|
+
<%= form.button("Next", type: :submit, class: "button") %>
|
|
26
|
+
<%= form.button(type: :button, class: "button button--secondary", data: { action: "webauthn-authentication#authenticate" }) do %>
|
|
27
|
+
<icon class="icon" data-icon="fingerprint" aria-label="Passkey"> </icon>
|
|
28
|
+
<% end %>
|
|
20
29
|
</div>
|
|
21
30
|
<% end %>
|
|
22
|
-
<%# note, autocomplete off is ignored by browsers but required by PCI-DSS %>
|
|
23
|
-
<%= form.govuk_email_field :email, autofocus: true, autocomplete: "off" %>
|
|
24
|
-
<%= form.hidden_field :response, data: { webauthn_authentication_target: "response" } %>
|
|
25
|
-
<%= hidden_field_tag(:redirect, redirect) %>
|
|
26
|
-
<div class="actions">
|
|
27
|
-
<%= form.admin_save "Next" %>
|
|
28
|
-
<%= form.button(type: :button, class: "button button--secondary", data: { action: "webauthn-authentication#authenticate" }) do %>
|
|
29
|
-
<icon class="icon" data-icon="fingerprint" aria-label="Passkey"> </icon>
|
|
30
|
-
<% end %>
|
|
31
|
-
</div>
|
|
32
31
|
<% end %>
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
<%# locals: (admin_user:) %>
|
|
2
2
|
|
|
3
|
-
<%=
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
<%= content_for(:roadblock) do %>
|
|
4
|
+
<header>
|
|
5
|
+
<icon aria-hidden="true" class="icon" data-icon="koi"> </icon>
|
|
6
|
+
<h1>Koi</h1>
|
|
7
|
+
<h2><%= Koi.config.site_name || URI.parse(root_url).host %></h2>
|
|
8
|
+
</header>
|
|
9
|
+
|
|
10
|
+
<%= form_with(model: admin_user, scope: :admin, url: admin_session_path, method: :post) do |form| %>
|
|
11
|
+
<%# note, autocomplete off is ignored by browsers but required by PCI-DSS %>
|
|
12
|
+
<%= form.govuk_text_field :token, autofocus: true, autocomplete: "off" %>
|
|
13
|
+
<%= hidden_field_tag(:redirect, params[:redirect]) %>
|
|
14
|
+
<%= form.button("Next", type: :submit, class: "button") %>
|
|
15
|
+
<% end %>
|
|
8
16
|
<% end %>
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
<%# locals: (admin_user:) %>
|
|
2
2
|
|
|
3
|
-
<%=
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
<%= content_for(:roadblock) do %>
|
|
4
|
+
<header>
|
|
5
|
+
<icon aria-hidden="true" class="icon" data-icon="koi"> </icon>
|
|
6
|
+
<h1>Koi</h1>
|
|
7
|
+
<h2><%= Koi.config.site_name || URI.parse(root_url).host %></h2>
|
|
8
|
+
</header>
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
<%= form_with(model: admin_user, scope: :admin, url: admin_session_path) do |form| %>
|
|
11
|
+
<%= form.hidden_field(:email) %>
|
|
12
|
+
<%# note, autocomplete off is ignored by browsers but required by PCI-DSS %>
|
|
13
|
+
<%= form.govuk_password_field :password, autofocus: true, autocomplete: "off" %>
|
|
14
|
+
<%= hidden_field_tag(:redirect, params[:redirect]) %>
|
|
15
|
+
<%= form.button("Next", type: :submit, class: "button") %>
|
|
16
|
+
|
|
17
|
+
<%# init govuk js to provide the show/hide button %>
|
|
18
|
+
<%= govuk_formbuilder_init %>
|
|
19
|
+
<% end %>
|
|
12
20
|
<% end %>
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
<%# locals: (admin_user:, token:) %>
|
|
2
2
|
|
|
3
|
-
<%=
|
|
4
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
<%= content_for(:roadblock) do %>
|
|
4
|
+
<header>
|
|
5
|
+
<icon aria-hidden="true" class="icon" data-icon="koi"> </icon>
|
|
6
|
+
<h1>Koi</h1>
|
|
7
|
+
<h2><%= Koi.config.site_name || URI.parse(root_url).host %></h2>
|
|
8
|
+
</header>
|
|
9
|
+
|
|
10
|
+
<p>Welcome, <%= admin_user %>.</p>
|
|
11
|
+
<p>Please sign in to continue.</p>
|
|
12
|
+
|
|
13
|
+
<%= form_with(url: admin_session_token_path(token), method: :patch) do |form| %>
|
|
14
|
+
<%= form.button("Sign in", type: :submit, class: "button") %>
|
|
8
15
|
<% end %>
|
|
9
|
-
<div class="actions">
|
|
10
|
-
<%= form.admin_save "Sign in" %>
|
|
11
|
-
</div>
|
|
12
16
|
<% end %>
|
|
@@ -13,15 +13,19 @@
|
|
|
13
13
|
data-action="input->navigation#filter change->navigation#filter
|
|
14
14
|
keydown.enter->navigation#go keydown.esc->navigation#clear">
|
|
15
15
|
</div>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
<
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
|
|
17
|
+
<% if Koi::Current.admin_user %>
|
|
18
|
+
<ul class="navigation-group | flow" role="list">
|
|
19
|
+
<li>
|
|
20
|
+
<span><%= Koi::Current.admin_user.name %></span>
|
|
21
|
+
<ul class="navigation-list | flow" role="list">
|
|
22
|
+
<li><%= link_to("Profile", main_app.admin_profile_path) %></li>
|
|
23
|
+
<li><%= link_to("Log out", main_app.admin_session_path, data: { turbo_method: :delete }) %></li>
|
|
24
|
+
</ul>
|
|
25
|
+
</li>
|
|
26
|
+
</ul>
|
|
27
|
+
<% end %>
|
|
28
|
+
|
|
25
29
|
<%= navigation_menu_with(
|
|
26
30
|
menu: Koi::Menu.admin_menu(self),
|
|
27
31
|
list: {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
ArrowRight->shortcut:page-next
|
|
43
43
|
">
|
|
44
44
|
<!-- application header -->
|
|
45
|
-
<%= render "layouts/koi/application_header" %>
|
|
45
|
+
<%= render "layouts/koi/application_header" unless content_for(:roadblock) %>
|
|
46
46
|
|
|
47
47
|
<!-- header -->
|
|
48
48
|
<% if content_for?(:header) %>
|
|
@@ -53,18 +53,27 @@
|
|
|
53
53
|
</header>
|
|
54
54
|
<% end %>
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
<!-- main -->
|
|
57
|
+
<% if content_for(:roadblock) %>
|
|
58
|
+
<main class="cover">
|
|
59
|
+
<div class="roadblock | flow wrapper">
|
|
60
|
+
<%= yield(:roadblock) %>
|
|
61
|
+
</div>
|
|
62
|
+
</main>
|
|
63
|
+
<% else %>
|
|
64
|
+
<main class="flow wrapper">
|
|
65
|
+
<% unless flash.empty? %>
|
|
66
|
+
<!-- flash -->
|
|
67
|
+
<%= render("layouts/koi/flash") %>
|
|
68
|
+
<% end %>
|
|
61
69
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
</main>
|
|
70
|
+
<!-- content -->
|
|
71
|
+
<%= yield %>
|
|
72
|
+
</main>
|
|
73
|
+
<% end %>
|
|
65
74
|
|
|
66
75
|
<!-- application navigation -->
|
|
67
|
-
<%= render
|
|
76
|
+
<%= render("layouts/koi/application_navigation") unless content_for(:roadblock) %>
|
|
68
77
|
|
|
69
78
|
</body>
|
|
70
79
|
</html>
|
data/config/locales/koi.en.yml
CHANGED
data/config/routes.rb
CHANGED
|
@@ -2,24 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
Rails.application.routes.draw do
|
|
4
4
|
namespace :admin do
|
|
5
|
-
resource :session, only: %i[new create destroy] do
|
|
6
|
-
# JWT tokens contain periods
|
|
7
|
-
resources :tokens, param: :token, only: %i[show update], token: /[^\/]+/
|
|
8
|
-
end
|
|
9
|
-
|
|
10
5
|
resources :admin_users do
|
|
11
|
-
resources :credentials, only: %i[new create destroy]
|
|
12
|
-
resource :otp, only: %i[new create destroy]
|
|
13
|
-
resources :tokens, only: %i[create]
|
|
14
6
|
get :archived, on: :collection
|
|
15
7
|
put :archive, on: :collection
|
|
16
8
|
put :restore, on: :collection
|
|
9
|
+
|
|
10
|
+
resources :tokens, only: %i[create]
|
|
17
11
|
end
|
|
18
12
|
|
|
19
13
|
resource :cache, only: %i[destroy]
|
|
20
14
|
resource :dashboard, only: %i[show]
|
|
21
|
-
|
|
15
|
+
|
|
16
|
+
resources :device_authorizations, param: :user_code, only: %i[create show update]
|
|
17
|
+
resources :device_tokens, only: %i[create]
|
|
18
|
+
|
|
19
|
+
resource :profile, only: %i[show edit update], shallow: true do
|
|
20
|
+
resources :credentials, only: %i[show new create update destroy]
|
|
21
|
+
resource :otp, only: %i[new create destroy]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
resource :session, only: %i[new create destroy] do
|
|
25
|
+
# JWT tokens contain periods
|
|
26
|
+
resources :tokens, param: :token, only: %i[show update], token: /[^\/]+/
|
|
27
|
+
end
|
|
28
|
+
|
|
22
29
|
resources :url_rewrites
|
|
30
|
+
resources :well_knowns
|
|
23
31
|
|
|
24
32
|
root to: redirect("admin/dashboard")
|
|
25
33
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateAdminDeviceAuthorizations < ActiveRecord::Migration[8.0]
|
|
4
|
+
def change
|
|
5
|
+
create_table :admin_device_authorizations do |t|
|
|
6
|
+
t.string :device_code_digest, null: false, index: { unique: true }
|
|
7
|
+
t.string :user_code, null: false, index: { unique: true }
|
|
8
|
+
t.string :status, null: false, default: "pending"
|
|
9
|
+
t.datetime :request_expires_at, null: false
|
|
10
|
+
t.datetime :approved_at
|
|
11
|
+
t.datetime :consumed_at
|
|
12
|
+
t.datetime :token_expires_at
|
|
13
|
+
t.references :admin_user, null: true, foreign_key: { to_table: :admins }
|
|
14
|
+
t.string :requested_ip
|
|
15
|
+
t.string :user_agent
|
|
16
|
+
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/koi/config.rb
CHANGED
data/lib/koi/engine.rb
CHANGED
|
@@ -19,27 +19,71 @@ module Koi
|
|
|
19
19
|
request = ActionDispatch::Request.new(env)
|
|
20
20
|
session = ActionDispatch::Request::Session.find(request)
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
# Always retrieve user to ensure we are not vulnerable to timing attacks
|
|
23
|
+
Koi::Current.admin_user = if bearer_token(request).present?
|
|
24
|
+
bearer_admin_user(request)
|
|
25
|
+
else
|
|
26
|
+
session_admin_user(session)
|
|
27
|
+
end
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
# Remove from session if not found
|
|
30
|
+
session.delete(:admin_user_id) if session.has_key?(:admin_user_id) && !authenticated?
|
|
31
|
+
|
|
32
|
+
if requires_authentication?(request) && !authenticated?
|
|
33
|
+
unauthorized_response(request)
|
|
30
34
|
else
|
|
31
35
|
@app.call(env)
|
|
32
36
|
end
|
|
37
|
+
ensure
|
|
38
|
+
Koi::Current.admin_user = nil
|
|
33
39
|
end
|
|
34
40
|
|
|
35
41
|
private
|
|
36
42
|
|
|
37
43
|
def requires_authentication?(request)
|
|
38
|
-
!request.path.starts_with?("/admin/session")
|
|
44
|
+
!request.path.starts_with?("/admin/session") && !device_flow_request?(request)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def authenticated?
|
|
48
|
+
Koi::Current.admin_user.present?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def bearer_admin_user(request)
|
|
52
|
+
token = bearer_token(request)
|
|
53
|
+
return if token.blank?
|
|
54
|
+
|
|
55
|
+
request.session_options[:skip] = true
|
|
56
|
+
|
|
57
|
+
Admin::User.find_by_token_for(:api_access, token)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def session_admin_user(session)
|
|
61
|
+
Admin::User.find_by(id: session[:admin_user_id])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def bearer_token(request)
|
|
65
|
+
return nil if request.authorization.blank?
|
|
66
|
+
|
|
67
|
+
request.authorization.match(/^Bearer (?<token>.+)$/)&.named_captures&.fetch("token", nil)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def unauthorized_response(request)
|
|
71
|
+
if bearer_token(request).present?
|
|
72
|
+
# If the user provided a token, it was not valid, and the request requires authentication
|
|
73
|
+
[401, {}, []]
|
|
74
|
+
else
|
|
75
|
+
if request.get?
|
|
76
|
+
# Set the redirection path for returning the user to their requested path after login
|
|
77
|
+
request.flash[:redirect] = request.fullpath
|
|
78
|
+
request.commit_flash
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
[303, { "Location" => "/admin/session/new" }, []]
|
|
82
|
+
end
|
|
39
83
|
end
|
|
40
84
|
|
|
41
|
-
def
|
|
42
|
-
|
|
85
|
+
def device_flow_request?(request)
|
|
86
|
+
request.post? && %w[/admin/device_authorizations /admin/device_tokens].include?(request.path)
|
|
43
87
|
end
|
|
44
88
|
end
|
|
45
89
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
FactoryBot.define do
|
|
4
|
+
factory :admin_device_authorization, class: "Admin::DeviceAuthorization" do
|
|
5
|
+
sequence(:device_code_digest) { |n| "digest-#{n}" }
|
|
6
|
+
sequence(:user_code) { |n| "CODE-#{n.to_s.rjust(4, '0')}" }
|
|
7
|
+
status { :pending }
|
|
8
|
+
request_expires_at { 10.minutes.from_now }
|
|
9
|
+
requested_ip { "127.0.0.1" }
|
|
10
|
+
user_agent { "RSpec" }
|
|
11
|
+
|
|
12
|
+
trait :approved do
|
|
13
|
+
status { :approved }
|
|
14
|
+
approved_at { Time.current }
|
|
15
|
+
admin_user
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
trait :denied do
|
|
19
|
+
status { :denied }
|
|
20
|
+
admin_user
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
trait :consumed do
|
|
24
|
+
status { :consumed }
|
|
25
|
+
consumed_at { Time.current }
|
|
26
|
+
admin_user
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: katalyst-koi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.
|
|
4
|
+
version: 5.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Katalyst Interactive
|
|
@@ -107,6 +107,20 @@ dependencies:
|
|
|
107
107
|
- - ">="
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
109
|
version: '0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: useragent
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
110
124
|
- !ruby/object:Gem::Dependency
|
|
111
125
|
name: webauthn
|
|
112
126
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -282,6 +296,7 @@ files:
|
|
|
282
296
|
- app/assets/stylesheets/koi/blocks/page-header.css
|
|
283
297
|
- app/assets/stylesheets/koi/blocks/pagy.css
|
|
284
298
|
- app/assets/stylesheets/koi/blocks/prose.css
|
|
299
|
+
- app/assets/stylesheets/koi/blocks/roadblock.css
|
|
285
300
|
- app/assets/stylesheets/koi/blocks/tables/index.css
|
|
286
301
|
- app/assets/stylesheets/koi/blocks/tables/query.css
|
|
287
302
|
- app/assets/stylesheets/koi/blocks/tables/table.css
|
|
@@ -316,7 +331,6 @@ files:
|
|
|
316
331
|
- app/assets/stylesheets/koi/global/variables.css
|
|
317
332
|
- app/assets/stylesheets/koi/icons.css
|
|
318
333
|
- app/assets/stylesheets/koi/index.css
|
|
319
|
-
- app/assets/stylesheets/koi/login.css
|
|
320
334
|
- app/assets/stylesheets/koi/utilities/index.css
|
|
321
335
|
- app/assets/stylesheets/koi/utilities/visually-hidden.css
|
|
322
336
|
- app/components/concerns/koi/tables/cells.rb
|
|
@@ -342,7 +356,10 @@ files:
|
|
|
342
356
|
- app/controllers/admin/caches_controller.rb
|
|
343
357
|
- app/controllers/admin/credentials_controller.rb
|
|
344
358
|
- app/controllers/admin/dashboards_controller.rb
|
|
359
|
+
- app/controllers/admin/device_authorizations_controller.rb
|
|
360
|
+
- app/controllers/admin/device_tokens_controller.rb
|
|
345
361
|
- app/controllers/admin/otps_controller.rb
|
|
362
|
+
- app/controllers/admin/profiles_controller.rb
|
|
346
363
|
- app/controllers/admin/sessions_controller.rb
|
|
347
364
|
- app/controllers/admin/tokens_controller.rb
|
|
348
365
|
- app/controllers/admin/url_rewrites_controller.rb
|
|
@@ -374,33 +391,35 @@ files:
|
|
|
374
391
|
- app/javascript/koi/elements/index.js
|
|
375
392
|
- app/javascript/koi/elements/toolbar.js
|
|
376
393
|
- app/javascript/koi/utils/transition.js
|
|
394
|
+
- app/jobs/admin/device_authorizations_cleanup_job.rb
|
|
377
395
|
- app/jobs/koi/application_job.rb
|
|
378
396
|
- app/models/admin/collection.rb
|
|
379
397
|
- app/models/admin/credential.rb
|
|
398
|
+
- app/models/admin/device_authorization.rb
|
|
380
399
|
- app/models/admin/user.rb
|
|
381
400
|
- app/models/application_record.rb
|
|
382
401
|
- app/models/concerns/koi/model/archivable.rb
|
|
383
402
|
- app/models/concerns/koi/model/otp.rb
|
|
403
|
+
- app/models/koi/current.rb
|
|
384
404
|
- app/models/url_rewrite.rb
|
|
385
405
|
- app/models/well_known.rb
|
|
386
|
-
- app/views/admin/admin_users/_form.html+self.erb
|
|
387
406
|
- app/views/admin/admin_users/_form.html.erb
|
|
388
407
|
- app/views/admin/admin_users/archived.html.erb
|
|
389
|
-
- app/views/admin/admin_users/edit.html+self.erb
|
|
390
408
|
- app/views/admin/admin_users/edit.html.erb
|
|
391
409
|
- app/views/admin/admin_users/index.html.erb
|
|
392
410
|
- app/views/admin/admin_users/new.html.erb
|
|
393
|
-
- app/views/admin/admin_users/show.html+self.erb
|
|
394
411
|
- app/views/admin/admin_users/show.html.erb
|
|
395
|
-
- app/views/admin/credentials/_credentials.html+self.erb
|
|
396
412
|
- app/views/admin/credentials/_credentials.html.erb
|
|
397
|
-
- app/views/admin/credentials/create.turbo_stream.erb
|
|
398
|
-
- app/views/admin/credentials/destroy.turbo_stream.erb
|
|
399
413
|
- app/views/admin/credentials/new.html.erb
|
|
414
|
+
- app/views/admin/credentials/show.html.erb
|
|
400
415
|
- app/views/admin/dashboards/show.html.erb
|
|
416
|
+
- app/views/admin/device_authorizations/show.html.erb
|
|
401
417
|
- app/views/admin/otps/_form.html.erb
|
|
402
418
|
- app/views/admin/otps/create.turbo_stream.erb
|
|
403
419
|
- app/views/admin/otps/new.html.erb
|
|
420
|
+
- app/views/admin/profiles/_form.html.erb
|
|
421
|
+
- app/views/admin/profiles/edit.html.erb
|
|
422
|
+
- app/views/admin/profiles/show.html.erb
|
|
404
423
|
- app/views/admin/sessions/new.html.erb
|
|
405
424
|
- app/views/admin/sessions/otp.html.erb
|
|
406
425
|
- app/views/admin/sessions/password.html.erb
|
|
@@ -433,7 +452,6 @@ files:
|
|
|
433
452
|
- app/views/layouts/koi/_navigation_item.html.erb
|
|
434
453
|
- app/views/layouts/koi/application.html.erb
|
|
435
454
|
- app/views/layouts/koi/frame.html.erb
|
|
436
|
-
- app/views/layouts/koi/login.html.erb
|
|
437
455
|
- config/brakeman.ignore
|
|
438
456
|
- config/importmap.rb
|
|
439
457
|
- config/initializers/extensions.rb
|
|
@@ -451,6 +469,7 @@ files:
|
|
|
451
469
|
- db/migrate/20231211005214_add_status_code_to_url_rewrites.rb
|
|
452
470
|
- db/migrate/20241214060913_add_otp_secret_to_admin_users.rb
|
|
453
471
|
- db/migrate/20250204060748_create_well_knowns.rb
|
|
472
|
+
- db/migrate/20260413014834_create_admin_device_authorizations.rb
|
|
454
473
|
- db/seeds.rb
|
|
455
474
|
- lib/generators/koi/admin/USAGE
|
|
456
475
|
- lib/generators/koi/admin/admin_generator.rb
|
|
@@ -487,6 +506,7 @@ files:
|
|
|
487
506
|
- lib/koi/middleware/admin_authentication.rb
|
|
488
507
|
- lib/koi/middleware/url_redirect.rb
|
|
489
508
|
- lib/koi/release.rb
|
|
509
|
+
- spec/factories/admin_device_authorizations.rb
|
|
490
510
|
- spec/factories/admins.rb
|
|
491
511
|
- spec/factories/url_rewrites.rb
|
|
492
512
|
- spec/factories/well_knowns.rb
|
|
@@ -509,7 +529,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
509
529
|
- !ruby/object:Gem::Version
|
|
510
530
|
version: '0'
|
|
511
531
|
requirements: []
|
|
512
|
-
rubygems_version: 4.0.
|
|
532
|
+
rubygems_version: 4.0.6
|
|
513
533
|
specification_version: 4
|
|
514
534
|
summary: Koi CMS admin framework
|
|
515
535
|
test_files: []
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
<%# locals: (admin_user:) %>
|
|
2
|
-
|
|
3
|
-
<%= table_with(id: dom_id(admin_user, :credentials), collection: admin_user.credentials) do |table, credential| %>
|
|
4
|
-
<% table.text :nickname, label: "Name" %>
|
|
5
|
-
<% table.date :updated_at, label: "Last use" do |date| %>
|
|
6
|
-
<%= date unless credential.created_at == credential.updated_at %>
|
|
7
|
-
<% end %>
|
|
8
|
-
<% table.cell :actions, label: "" do %>
|
|
9
|
-
<%= link_to("Remove passkey", admin_admin_user_credential_path(admin_user, credential),
|
|
10
|
-
data: { turbo_method: :delete }) %>
|
|
11
|
-
<% end %>
|
|
12
|
-
<% end %>
|