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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -12
  3. data/app/assets/builds/katalyst/koi.esm.js +9 -19
  4. data/app/assets/builds/katalyst/koi.js +9 -19
  5. data/app/assets/builds/katalyst/koi.min.js +1 -1
  6. data/app/assets/builds/katalyst/koi.min.js.map +1 -1
  7. data/app/assets/stylesheets/koi/blocks/index.css +1 -0
  8. data/app/assets/stylesheets/koi/{login.css → blocks/roadblock.css} +8 -5
  9. data/app/controllers/admin/admin_users_controller.rb +3 -2
  10. data/app/controllers/admin/credentials_controller.rb +33 -51
  11. data/app/controllers/admin/device_authorizations_controller.rb +58 -0
  12. data/app/controllers/admin/device_tokens_controller.rb +18 -0
  13. data/app/controllers/admin/otps_controller.rb +6 -16
  14. data/app/controllers/admin/profiles_controller.rb +33 -0
  15. data/app/controllers/admin/sessions_controller.rb +12 -6
  16. data/app/controllers/admin/tokens_controller.rb +6 -2
  17. data/app/controllers/concerns/koi/controller/has_admin_users.rb +8 -11
  18. data/app/controllers/concerns/koi/controller/has_webauthn.rb +54 -9
  19. data/app/controllers/concerns/koi/controller.rb +7 -0
  20. data/app/javascript/koi/controllers/webauthn_authentication_controller.js +1 -1
  21. data/app/javascript/koi/controllers/webauthn_registration_controller.js +8 -18
  22. data/app/jobs/admin/device_authorizations_cleanup_job.rb +9 -0
  23. data/app/models/admin/credential.rb +4 -0
  24. data/app/models/admin/device_authorization.rb +113 -0
  25. data/app/models/admin/user.rb +1 -0
  26. data/app/models/koi/current.rb +8 -0
  27. data/app/views/admin/admin_users/edit.html.erb +3 -1
  28. data/app/views/admin/admin_users/show.html.erb +3 -0
  29. data/app/views/admin/credentials/_credentials.html.erb +8 -5
  30. data/app/views/admin/credentials/new.html.erb +32 -41
  31. data/app/views/admin/credentials/show.html.erb +19 -0
  32. data/app/views/admin/device_authorizations/show.html.erb +38 -0
  33. data/app/views/admin/otps/_form.html.erb +1 -1
  34. data/app/views/admin/{admin_users/_form.html+self.erb → profiles/_form.html.erb} +1 -1
  35. data/app/views/admin/{admin_users/edit.html+self.erb → profiles/edit.html.erb} +0 -1
  36. data/app/views/admin/{admin_users/show.html+self.erb → profiles/show.html.erb} +15 -10
  37. data/app/views/admin/sessions/new.html.erb +26 -27
  38. data/app/views/admin/sessions/otp.html.erb +13 -5
  39. data/app/views/admin/sessions/password.html.erb +16 -8
  40. data/app/views/admin/tokens/show.html.erb +12 -8
  41. data/app/views/layouts/koi/_application_navigation.html.erb +13 -9
  42. data/app/views/layouts/koi/application.html.erb +19 -10
  43. data/config/locales/koi.en.yml +0 -1
  44. data/config/routes.rb +17 -9
  45. data/db/migrate/20260413014834_create_admin_device_authorizations.rb +20 -0
  46. data/lib/generators/koi/helpers/resource_helpers.rb +1 -1
  47. data/lib/koi/config.rb +2 -1
  48. data/lib/koi/engine.rb +1 -0
  49. data/lib/koi/middleware/admin_authentication.rb +54 -10
  50. data/spec/factories/admin_device_authorizations.rb +29 -0
  51. metadata +30 -10
  52. data/app/views/admin/credentials/_credentials.html+self.erb +0 -12
  53. data/app/views/admin/credentials/create.turbo_stream.erb +0 -5
  54. data/app/views/admin/credentials/destroy.turbo_stream.erb +0 -5
  55. data/app/views/layouts/koi/login.html.erb +0 -50
@@ -2,7 +2,6 @@
2
2
 
3
3
  <% content_for(:header) do %>
4
4
  <%= breadcrumb_list do %>
5
- <li><%= link_to("Admin users", admin_admin_users_path) %></li>
6
5
  <li><%= link_to(admin_user, admin_admin_user_path(admin_user)) %></li>
7
6
  <% end %>
8
7
 
@@ -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", edit_admin_admin_user_path(admin_user)) %></li>
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", admin_admin_user_otp_path(admin_user),
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", new_admin_admin_user_otp_path(admin_user),
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
- <%= link_to("New passkey", new_admin_admin_user_credential_path(admin_user),
40
- class: "button", data: { turbo_frame: "edit" }) %>
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 "admin/credentials/credentials", admin_user: %>
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
- <%= form_with(
4
- model: admin_user,
5
- scope: :admin,
6
- url: admin_session_path,
7
- data: {
8
- controller: "webauthn-authentication",
9
- webauthn_authentication_options_value: { publicKey: webauthn_auth_options },
10
- },
11
- ) do |form| %>
12
- <% (redirect = flash[:redirect] || params[:redirect]) && flash.delete(:redirect) %>
13
- <% unless flash.empty? %>
14
- <div class="govuk-error-summary">
15
- <ul class="govuk-error-summary__list">
16
- <% flash.each do |type, message| %>
17
- <%= tag.li message %>
18
- <% end %>
19
- </ul>
3
+ <%= content_for(:roadblock) do %>
4
+ <header>
5
+ <icon aria-hidden="true" class="icon" data-icon="koi">&nbsp;</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">&nbsp;</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">&nbsp;</icon>
30
- <% end %>
31
- </div>
32
31
  <% end %>
@@ -1,8 +1,16 @@
1
1
  <%# locals: (admin_user:) %>
2
2
 
3
- <%= form_with(model: admin_user, scope: :admin, url: admin_session_path, method: :post) do |form| %>
4
- <%# note, autocomplete off is ignored by browsers but required by PCI-DSS %>
5
- <%= form.govuk_text_field :token, autofocus: true, autocomplete: "off" %>
6
- <%= hidden_field_tag(:redirect, params[:redirect]) %>
7
- <%= form.admin_save "Next" %>
3
+ <%= content_for(:roadblock) do %>
4
+ <header>
5
+ <icon aria-hidden="true" class="icon" data-icon="koi">&nbsp;</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
- <%= form_with(model: admin_user, scope: :admin, url: admin_session_path) do |form| %>
4
- <%= form.hidden_field(:email) %>
5
- <%# note, autocomplete off is ignored by browsers but required by PCI-DSS %>
6
- <%= form.govuk_password_field :password, autofocus: true, autocomplete: "off" %>
7
- <%= hidden_field_tag(:redirect, params[:redirect]) %>
8
- <%= form.admin_save "Next" %>
3
+ <%= content_for(:roadblock) do %>
4
+ <header>
5
+ <icon aria-hidden="true" class="icon" data-icon="koi">&nbsp;</icon>
6
+ <h1>Koi</h1>
7
+ <h2><%= Koi.config.site_name || URI.parse(root_url).host %></h2>
8
+ </header>
9
9
 
10
- <%# init govuk js to provide the show/hide button %>
11
- <%= govuk_formbuilder_init %>
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
- <%= form_with(url: admin_session_token_path(token), method: :patch) do |form| %>
4
- <p>Welcome to Koi Admin</p>
5
- <%= summary_table_with(model: admin_user) do |row| %>
6
- <%= row.text :name %>
7
- <%= row.text :email %>
3
+ <%= content_for(:roadblock) do %>
4
+ <header>
5
+ <icon aria-hidden="true" class="icon" data-icon="koi">&nbsp;</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
- <ul class="navigation-group | flow" role="list">
17
- <li>
18
- <span><%= current_admin_user.name %></span>
19
- <ul class="navigation-list | flow" role="list">
20
- <li><%= link_to("Profile", main_app.admin_admin_user_path(current_admin_user)) %></li>
21
- <li><%= link_to("Log out", main_app.admin_session_path, data: { turbo_method: :delete }) %></li>
22
- </ul>
23
- </li>
24
- </ul>
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
- <main class="flow wrapper">
57
- <% unless flash.empty? %>
58
- <!-- flash -->
59
- <%= render "layouts/koi/flash" %>
60
- <% end %>
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
- <!-- content -->
63
- <%= yield %>
64
- </main>
70
+ <!-- content -->
71
+ <%= yield %>
72
+ </main>
73
+ <% end %>
65
74
 
66
75
  <!-- application navigation -->
67
- <%= render "layouts/koi/application_navigation" %>
76
+ <%= render("layouts/koi/application_navigation") unless content_for(:roadblock) %>
68
77
 
69
78
  </body>
70
79
  </html>
@@ -11,7 +11,6 @@ en:
11
11
  auth:
12
12
  otp_app_name: "%{host}/admin"
13
13
  token_invalid: "Token invalid or consumed already"
14
- token_consumed: "Please create a password or passkey"
15
14
  labels:
16
15
  new: New
17
16
  search: Search
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
- resources :well_knowns
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
@@ -58,7 +58,7 @@ module Koi
58
58
  private
59
59
 
60
60
  def archivable?
61
- model_class&.included_modules&.include?(Koi::Model::Archivable)
61
+ model_class&.include?(Koi::Model::Archivable)
62
62
  end
63
63
 
64
64
  def orderable?
data/lib/koi/config.rb CHANGED
@@ -12,7 +12,8 @@ module Koi
12
12
  :document_mime_types,
13
13
  :document_size_limit,
14
14
  :image_mime_types,
15
- :image_size_limit
15
+ :image_size_limit,
16
+ :site_name
16
17
 
17
18
  def initialize
18
19
  @admin_name = "Koi"
data/lib/koi/engine.rb CHANGED
@@ -10,6 +10,7 @@ require "rotp"
10
10
  require "rqrcode"
11
11
  require "stimulus-rails"
12
12
  require "turbo-rails"
13
+ require "useragent"
13
14
  require "webauthn"
14
15
 
15
16
  module Koi
@@ -19,27 +19,71 @@ module Koi
19
19
  request = ActionDispatch::Request.new(env)
20
20
  session = ActionDispatch::Request::Session.find(request)
21
21
 
22
- if requires_authentication?(request) && !authenticated?(session)
23
- # Set the redirection path for returning the user to their requested path after login
24
- if request.get?
25
- request.flash[:redirect] = request.fullpath
26
- request.commit_flash
27
- end
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
- [303, { "Location" => "/admin/session/new" }, []]
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 authenticated?(session)
42
- session[:admin_user_id].present?
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.3.1
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.3
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 %>
@@ -1,5 +0,0 @@
1
- <%# locals: (admin_user:) %>
2
-
3
- <%= turbo_stream.replace(dom_id(admin_user, :credentials)) do %>
4
- <%= render "admin/credentials/credentials", admin_user: %>
5
- <% end %>
@@ -1,5 +0,0 @@
1
- <%# locals: (admin_user:) %>
2
-
3
- <%= turbo_stream.replace(dom_id(admin_user, :credentials)) do %>
4
- <%= render "admin/credentials/credentials", admin_user: %>
5
- <% end %>