katalyst-koi 4.13.2 → 4.14.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/koi/admin.css +1 -1
  3. data/app/assets/javascripts/koi/controllers/webauthn_registration_controller.js +15 -7
  4. data/app/assets/stylesheets/koi/base/_flow.scss +8 -0
  5. data/app/assets/stylesheets/koi/base/_index.scss +2 -0
  6. data/app/assets/stylesheets/koi/base/_repel.scss +23 -0
  7. data/app/controllers/admin/admin_users_controller.rb +2 -0
  8. data/app/controllers/admin/credentials_controller.rb +5 -1
  9. data/app/controllers/admin/sessions_controller.rb +12 -7
  10. data/app/controllers/admin/tokens_controller.rb +28 -23
  11. data/app/controllers/concerns/koi/controller/has_webauthn.rb +2 -1
  12. data/app/controllers/concerns/koi/controller/is_admin_controller.rb +6 -5
  13. data/app/views/admin/admin_users/_fields.html+self.erb +3 -0
  14. data/app/views/admin/admin_users/_fields.html.erb +0 -1
  15. data/app/views/admin/admin_users/index.html.erb +3 -0
  16. data/app/views/admin/admin_users/show.html+self.erb +19 -0
  17. data/app/views/admin/admin_users/show.html.erb +9 -13
  18. data/app/views/admin/credentials/_credentials.html+self.erb +10 -0
  19. data/app/views/admin/credentials/_credentials.html.erb +4 -5
  20. data/app/views/admin/credentials/new.html.erb +27 -4
  21. data/app/views/admin/sessions/new.html.erb +6 -3
  22. data/app/views/admin/tokens/create.turbo_stream.erb +1 -1
  23. data/app/views/admin/tokens/show.html.erb +1 -2
  24. data/app/views/layouts/koi/_navigation.html.erb +9 -0
  25. data/config/locales/koi.en.yml +1 -2
  26. data/config/routes.rb +6 -11
  27. data/lib/koi/engine.rb +1 -0
  28. data/lib/koi/menu.rb +0 -1
  29. data/lib/koi/middleware/admin_authentication.rb +46 -0
  30. metadata +9 -3
@@ -6,8 +6,11 @@ import {
6
6
  } from "@github/webauthn-json/browser-ponyfill";
7
7
 
8
8
  export default class WebauthnRegistrationController extends Controller {
9
- static values = { options: Object };
10
- static targets = ["response"];
9
+ static values = {
10
+ options: Object,
11
+ response: String,
12
+ };
13
+ static targets = ["intro", "nickname", "response"];
11
14
 
12
15
  submit(e) {
13
16
  if (this.responseTarget.value === "") {
@@ -16,12 +19,17 @@ export default class WebauthnRegistrationController extends Controller {
16
19
  }
17
20
  }
18
21
 
19
- createCredential() {
20
- create(this.options).then((response) => {
21
- this.responseTarget.value = JSON.stringify(response);
22
+ async createCredential() {
23
+ const response = await create(this.options);
22
24
 
23
- this.element.requestSubmit();
24
- });
25
+ this.responseValue = JSON.stringify(response);
26
+ this.responseTarget.value = JSON.stringify(response);
27
+ }
28
+
29
+ responseValueChanged(response) {
30
+ const responsePresent = response !== "";
31
+ this.introTarget.toggleAttribute("hidden", responsePresent);
32
+ this.nicknameTarget.toggleAttribute("hidden", !responsePresent);
25
33
  }
26
34
 
27
35
  get options() {
@@ -0,0 +1,8 @@
1
+ /*
2
+ FLOW COMPOSITION
3
+ Like the Every Layout stack: https://every-layout.dev/layouts/stack/
4
+ Info about this implementation: https://piccalil.li/quick-tip/flow-utility/
5
+ */
6
+ .flow > * + * {
7
+ margin-top: var(--flow-space, 1em);
8
+ }
@@ -1,8 +1,10 @@
1
1
  @use "button";
2
2
  @use "icon";
3
3
  @use "input";
4
+ @use "flow";
4
5
  @use "link";
5
6
  @use "list";
7
+ @use "repel";
6
8
  @use "tables";
7
9
  @use "typography";
8
10
 
@@ -0,0 +1,23 @@
1
+ /*
2
+ REPEL
3
+ A little layout that pushes items away from each other where
4
+ there is space in the viewport and stacks on small viewports
5
+
6
+ CUSTOM PROPERTIES AND CONFIGURATION
7
+ --gutter (var(--space-s-m)): This defines the space
8
+ between each item.
9
+
10
+ --repel-vertical-alignment How items should align vertically.
11
+ Can be any acceptable flexbox alignment value.
12
+ */
13
+ .repel {
14
+ display: flex;
15
+ flex-wrap: wrap;
16
+ justify-content: space-between;
17
+ align-items: var(--repel-vertical-alignment, center);
18
+ gap: var(--gutter, var(--space-s-m));
19
+ }
20
+
21
+ .repel[data-nowrap] {
22
+ flex-wrap: nowrap;
23
+ }
@@ -72,6 +72,8 @@ module Admin
72
72
 
73
73
  def set_admin
74
74
  @admin = Admin::User.with_archived.find(params[:id])
75
+
76
+ request.variant << :self if @admin == current_admin_user
75
77
  end
76
78
 
77
79
  def admin_user_params
@@ -70,7 +70,11 @@ module Admin
70
70
  def set_admin_user
71
71
  @admin_user = Admin::User.find(params[:admin_user_id])
72
72
 
73
- head(:forbidden) unless current_admin == @admin_user
73
+ if current_admin == @admin_user
74
+ request.variant = :self
75
+ else
76
+ head(:forbidden)
77
+ end
74
78
  end
75
79
  end
76
80
  end
@@ -4,14 +4,13 @@ module Admin
4
4
  class SessionsController < ApplicationController
5
5
  include Koi::Controller::HasWebauthn
6
6
 
7
- skip_before_action :authenticate_admin, only: %i[new create]
7
+ before_action :redirect_authenticated, only: %i[new], if: :admin_signed_in?
8
+ before_action :authenticate_local_admin, only: %i[new], if: :authenticate_local_admins?
8
9
 
9
10
  layout "koi/login"
10
11
 
11
12
  def new
12
- return redirect_to admin_dashboard_path if admin_signed_in?
13
-
14
- render :new, locals: { admin_user: Admin::User.new }
13
+ render locals: { admin_user: Admin::User.new }
15
14
  end
16
15
 
17
16
  def create
@@ -20,12 +19,12 @@ module Admin
20
19
 
21
20
  session[:admin_user_id] = admin_user.id
22
21
 
23
- redirect_to admin_dashboard_path, notice: I18n.t("koi.auth.login")
22
+ redirect_to(url_from(params[:redirect].presence) || admin_dashboard_path, status: :see_other)
24
23
  else
25
24
  admin_user = Admin::User.new(session_params.slice(:email, :password))
26
25
  admin_user.errors.add(:email, "Invalid email or password")
27
26
 
28
- render :new, status: :unprocessable_content, locals: { admin_user: }
27
+ render(:new, status: :unprocessable_content, locals: { admin_user: })
29
28
  end
30
29
  end
31
30
 
@@ -34,11 +33,15 @@ module Admin
34
33
 
35
34
  session[:admin_user_id] = nil
36
35
 
37
- redirect_to admin_dashboard_path, notice: I18n.t("koi.auth.logout")
36
+ redirect_to new_admin_session_path
38
37
  end
39
38
 
40
39
  private
41
40
 
41
+ def redirect_authenticated
42
+ redirect_to(admin_dashboard_path, status: :see_other)
43
+ end
44
+
42
45
  def session_params
43
46
  params.require(:admin).permit(:email, :password, :response)
44
47
  end
@@ -65,6 +68,8 @@ module Admin
65
68
  end
66
69
 
67
70
  def record_sign_out!(admin_user)
71
+ return unless admin_user
72
+
68
73
  update_last_sign_in(admin_user)
69
74
 
70
75
  admin_user.current_sign_in_at = nil
@@ -4,49 +4,54 @@ module Admin
4
4
  class TokensController < ApplicationController
5
5
  include Koi::Controller::JsonWebToken
6
6
 
7
- skip_before_action :authenticate_admin, only: %i[show update]
7
+ before_action :set_admin, only: %i[create]
8
8
  before_action :set_token, only: %i[show update]
9
+ before_action :invalid_token, only: %i[show update], unless: :token_valid?
9
10
 
10
11
  def show
11
- return redirect_to new_admin_session_path, notice: I18n.t("koi.auth.token_invalid") if @token.blank?
12
-
13
- admin = Admin::User.find(@token[:admin_id])
14
-
15
- if token_utilised?(admin, @token)
16
- return redirect_to new_admin_session_path, notice: I18n.t("koi.auth.token_invalid")
17
- end
18
-
19
- render locals: { admin:, token: params[:token] }, layout: "koi/login"
12
+ render locals: { admin: @admin, token: params[:token] }, layout: "koi/login"
20
13
  end
21
14
 
22
15
  def create
23
- admin = Admin::User.find(params[:id])
24
- token = encode_token(admin_id: admin.id, exp: 5.minutes.from_now.to_i, iat: Time.current.to_i)
16
+ token = encode_token(admin_id: @admin.id, exp: 30.minutes.from_now.to_i, iat: Time.current.to_i)
25
17
 
26
18
  render locals: { token: }
27
19
  end
28
20
 
29
21
  def update
30
- return redirect_to admin_dashboard_path, status: :see_other if admin_signed_in?
31
-
32
- if @token.blank?
33
- return redirect_to new_admin_session_path, status: :see_other, notice: I18n.t("koi.auth.token_invalid")
34
- end
35
-
36
- admin = Admin::User.find(@token[:admin_id])
37
- sign_in_admin(admin)
22
+ sign_in_admin(@admin)
38
23
 
39
- redirect_to admin_admin_user_path(admin)
24
+ redirect_to admin_admin_user_path(@admin), status: :see_other, notice: t("koi.auth.token_consumed")
40
25
  end
41
26
 
42
27
  private
43
28
 
29
+ def set_admin
30
+ @admin = Admin::User.find(params[:admin_user_id])
31
+ end
32
+
44
33
  def set_token
45
34
  @token = decode_token(params[:token])
35
+
36
+ # constant time token validation requires that we always try to retrieve a user
37
+ @admin = Admin::User.find_by(id: @token&.fetch(:admin_id) || "")
38
+ end
39
+
40
+ def token_valid?
41
+ return false unless @token.present? && @admin.present?
42
+
43
+ # Ensure that the user has not signed in since the token was generated
44
+ if @admin.current_sign_in_at.present?
45
+ @admin.current_sign_in_at.to_i < @token[:iat]
46
+ elsif @admin.last_sign_in_at.present?
47
+ @admin.last_sign_in_at.to_i < @token[:iat]
48
+ else
49
+ true # first sign in
50
+ end
46
51
  end
47
52
 
48
- def token_utilised?(admin, token)
49
- admin.current_sign_in_at.present? || (admin.last_sign_in_at.present? && admin.last_sign_in_at.to_i > token[:iat])
53
+ def invalid_token
54
+ redirect_to(new_admin_session_path, status: :see_other, notice: I18n.t("koi.auth.token_invalid"))
50
55
  end
51
56
 
52
57
  def sign_in_admin(admin)
@@ -36,7 +36,8 @@ module Koi
36
36
  Admin::Credential.find_by!(external_id: credential.id)
37
37
  end
38
38
 
39
- stored_credential.update!(sign_count: webauthn_credential.sign_count)
39
+ stored_credential.update(sign_count: webauthn_credential.sign_count)
40
+ stored_credential.touch
40
41
 
41
42
  stored_credential.admin
42
43
  end
@@ -31,9 +31,6 @@ module Koi
31
31
  helper :all
32
32
 
33
33
  layout -> { turbo_frame_layout || "koi/application" }
34
-
35
- before_action :authenticate_local_admin, if: -> { Koi::Controller::IsAdminController.authenticate_local_admins }
36
- before_action :authenticate_admin, unless: :admin_signed_in?
37
34
  end
38
35
 
39
36
  class << self
@@ -47,10 +44,14 @@ module Koi
47
44
 
48
45
  session[:admin_user_id] =
49
46
  Admin::User.where(email: %W[#{ENV.fetch('USER', nil)}@katalyst.com.au admin@katalyst.com.au]).first&.id
47
+
48
+ flash.delete(:redirect) if (redirect = flash[:redirect])
49
+
50
+ redirect_to(redirect || admin_dashboard_path, status: :see_other)
50
51
  end
51
52
 
52
- def authenticate_admin
53
- redirect_to new_admin_session_path, status: :temporary_redirect
53
+ def authenticate_local_admins?
54
+ IsAdminController.authenticate_local_admins
54
55
  end
55
56
 
56
57
  def turbo_frame_layout
@@ -0,0 +1,3 @@
1
+ <%= form.govuk_text_field :email %>
2
+ <%= form.govuk_text_field :name %>
3
+ <%= form.govuk_password_field :password, label: { text: "Password (optional)" } %>
@@ -1,4 +1,3 @@
1
1
  <%= form.govuk_text_field :email %>
2
2
  <%= form.govuk_text_field :name %>
3
- <%= form.govuk_password_field :password, label: { text: "Password#{' (optional)' if form.object.persisted?}" } %>
4
3
  <%= form.govuk_check_box_field :archived if form.object.persisted? %>
@@ -15,6 +15,9 @@
15
15
  <% row.select %>
16
16
  <% row.link :name, url: :admin_admin_user_path %>
17
17
  <% row.text :email %>
18
+ <% row.boolean :credentials, label: "Passkey" do |cell| %>
19
+ <%= cell.value.any? ? "Yes" : "No" %>
20
+ <% end %>
18
21
  <% end %>
19
22
 
20
23
  <%= table_pagination_with(collection:) %>
@@ -0,0 +1,19 @@
1
+ <%# locals: (admin:) %>
2
+
3
+ <% content_for :header do %>
4
+ <%= render Koi::Header::ShowComponent.new(resource: admin) %>
5
+ <% end %>
6
+
7
+ <%= render Koi::SummaryListComponent.new(model: admin, class: "item-table") do |builder| %>
8
+ <%= builder.text :name %>
9
+ <%= builder.text :email %>
10
+ <%= builder.date :created_at %>
11
+ <%= builder.date :last_sign_in_at, label: { text: "Last sign in" } %>
12
+ <% end %>
13
+
14
+ <div class="repel">
15
+ <h3>Passkeys</h3>
16
+ <%= kpop_link_to "New passkey", new_admin_admin_user_credential_path(admin), class: "button button--primary" %>
17
+ </div>
18
+
19
+ <%= render "admin/credentials/credentials", admin: %>
@@ -1,3 +1,5 @@
1
+ <%# locals: (admin:) %>
2
+
1
3
  <% content_for :header do %>
2
4
  <%= render Koi::Header::ShowComponent.new(resource: admin) %>
3
5
  <% end %>
@@ -5,11 +7,15 @@
5
7
  <%= render Koi::SummaryListComponent.new(model: admin, class: "item-table") do |builder| %>
6
8
  <%= builder.text :name %>
7
9
  <%= builder.text :email %>
8
- <%= builder.datetime :created_at %>
9
- <%= builder.datetime :last_sign_in_at, label: { text: "Last sign in" } %>
10
+ <%= builder.date :created_at %>
11
+ <%= builder.date :last_sign_in_at, label: { text: "Last sign in" } %>
10
12
  <%= builder.boolean :archived? %>
11
13
  <% end %>
12
14
 
15
+ <h3>Passkeys</h3>
16
+
17
+ <%= render "admin/credentials/credentials", admin: %>
18
+
13
19
  <div class="actions">
14
20
  <% if admin.archived? %>
15
21
  <%= button_to "Delete", admin_admin_user_path(admin),
@@ -17,15 +23,5 @@
17
23
  method: :delete,
18
24
  form: { data: { turbo_confirm: "Are you sure?" } } %>
19
25
  <% end %>
20
- <%= button_to "Generate login link", invite_admin_admin_user_path(admin), class: "button button--primary", form: { id: "invite" } %>
26
+ <%= button_to "Generate login link", admin_admin_user_tokens_path(admin), class: "button button--primary", form: { id: "invite" } %>
21
27
  </div>
22
-
23
- <h2>Authentication</h2>
24
-
25
- <%= render "admin/credentials/credentials", admin: %>
26
-
27
- <% if admin == current_admin %>
28
- <div class="actions-group">
29
- <%= kpop_link_to "Add this device", new_admin_admin_user_credential_path(admin), class: "button button--primary" %>
30
- </div>
31
- <% end %>
@@ -0,0 +1,10 @@
1
+ <%= table_with(id: dom_id(admin, :credentials), collection: admin.credentials) do |t, c| %>
2
+ <% t.text :nickname, label: "Name" %>
3
+ <% t.date :updated_at, label: "Last use" do |date| %>
4
+ <%= date unless c.created_at == c.updated_at %>
5
+ <% end %>
6
+ <% t.cell :actions, label: "" do %>
7
+ <%= link_to("Remove passkey", admin_admin_user_credential_path(admin, c),
8
+ data: { turbo_method: :delete }) %>
9
+ <% end %>
10
+ <% end %>
@@ -1,7 +1,6 @@
1
1
  <%= table_with(id: dom_id(admin, :credentials), collection: admin.credentials) do |t, c| %>
2
- <% t.text :nickname %>
3
- <% t.number :sign_count %>
4
- <% t.cell :actions, label: "" do %>
5
- <%= button_to "Remove device", admin_admin_user_credential_path(admin, c), method: :delete, class: "button button--text" %>
6
- <% end if admin == current_admin %>
2
+ <% t.text :nickname, label: "Name" %>
3
+ <% t.date :updated_at, label: "Last use" do |date| %>
4
+ <%= date unless c.created_at == c.updated_at %>
5
+ <% end %>
7
6
  <% end %>
@@ -1,14 +1,37 @@
1
- <%= render Kpop::ModalComponent.new(title: "Register device") do %>
1
+ <%= render Kpop::ModalComponent.new(title: "New passkey") do %>
2
2
  <%= form_with model: admin.credentials.new,
3
3
  url: admin_admin_user_credentials_path(admin),
4
+ class: "flow prose",
4
5
  data: {
5
6
  controller: "webauthn-registration",
6
7
  action: "submit->webauthn-registration#submit",
7
8
  webauthn_registration_options_value: { publicKey: options },
8
9
  } do |form| %>
9
- <%= form.govuk_text_field :nickname %>
10
10
  <%= form.hidden_field :response, data: { webauthn_registration_target: "response" } %>
11
-
12
- <%= form.admin_save %>
11
+ <section class="flow prose" data-webauthn-registration-target="intro">
12
+ <p>
13
+ Passkeys are secure secrets that are stored by your device.
14
+ You will need the device where your passkey is stored to log in.
15
+ </p>
16
+ <p>
17
+ Unlike a password, your password doesn't get sent to the server when you log
18
+ in and can't be stolen in a data breach. When you log in with a passkey,
19
+ your operating system will prompt you for permission to use the passkey
20
+ secret to authenticate the login attempt.
21
+ </p>
22
+ <p>
23
+ We recommend that you store your passkey on your phone or cloud account.
24
+ Depending on your browser, you may need to choose "more options" to see
25
+ a QR code that you can scan with your phone.
26
+ </p>
27
+ </section>
28
+ <section class="flow" data-webauthn-registration-target="nickname" hidden>
29
+ <%= form.govuk_text_field :nickname, label: { text: "Passkey name" } do %>
30
+ Enter a name for this passkey to help you distinguish it from other passkeys you may have for this site.
31
+ <br>
32
+ Example: My Phone, Chrome, iCloud, 1Password
33
+ <% end %>
34
+ </section>
35
+ <%= form.admin_save("Next") %>
13
36
  <% end %>
14
37
  <% end %>
@@ -7,19 +7,22 @@
7
7
  webauthn_authentication_options_value: { publicKey: webauthn_auth_options },
8
8
  },
9
9
  ) do |f| %>
10
+ <% (redirect = flash[:redirect] || params[:redirect]) && flash.delete(:redirect) %>
10
11
  <% unless flash.empty? %>
11
12
  <div class="govuk-error-summary">
12
13
  <ul class="govuk-error-summary__list">
13
- <% flash.each do |_, message| %>
14
+ <% flash.each do |type, message| %>
14
15
  <%= tag.li message %>
15
16
  <% end %>
16
17
  </ul>
17
18
  </div>
18
19
  <% end %>
19
20
  <%= f.govuk_fieldset legend: nil do %>
20
- <%= f.govuk_email_field :email, autofocus: true, autocomplete: "email" %>
21
- <%= f.govuk_password_field :password, autocomplete: "current-password" %>
21
+ <%# note, autocomplete off is ignored by browsers but required by PCI-DSS %>
22
+ <%= f.govuk_email_field :email, autofocus: true, autocomplete: "off" %>
23
+ <%= f.govuk_password_field :password, autocomplete: "off" %>
22
24
  <%= f.hidden_field :response, data: { webauthn_authentication_target: "response" } %>
25
+ <%= hidden_field_tag(:redirect, redirect) %>
23
26
  <% end %>
24
27
  <div class="actions-group">
25
28
  <%= f.admin_save "Log in" %>
@@ -1,6 +1,6 @@
1
1
  <%= turbo_stream.replace "invite" do %>
2
2
  <div class="action copy-to-clipboard govuk-input__wrapper" data-controller="clipboard" data-clipboard-supported-class="clipboard--supported">
3
- <%= text_field_tag :invite_link, admin_token_url(token), readonly: true, data: { clipboard_target: "source" } %>
3
+ <%= text_field_tag :invite_link, admin_session_token_url(token), readonly: true, data: { clipboard_target: "source" } %>
4
4
  <button class="govuk-input__suffix clipboard-button" aria-hidden="true" data-action="clipboard#copy">
5
5
  Copy link
6
6
  </button>
@@ -1,7 +1,6 @@
1
1
  <%= render "layouts/koi/navigation_header" %>
2
2
 
3
- <%= form_with(url: accept_admin_session_path) do |form| %>
4
- <%= tag.input name: :token, type: :hidden, value: token %>
3
+ <%= form_with(url: admin_session_token_path(token), method: :patch) do |form| %>
5
4
  <p>Welcome to Koi Admin</p>
6
5
  <%= render Koi::SummaryListComponent.new(model: admin, class: "item-table") do |builder| %>
7
6
  <%= builder.text :name %>
@@ -9,6 +9,15 @@
9
9
  data-action="input->navigation#filter change->navigation#filter
10
10
  keydown.enter->navigation#go keydown.esc->navigation#clear">
11
11
  </div>
12
+ <ul role="list">
13
+ <li>
14
+ <span><%= current_admin_user.name %></span>
15
+ <ul role="list">
16
+ <li><%= link_to("Profile", admin_admin_user_path(current_admin_user)) %></li>
17
+ <li><%= link_to("Log out", admin_session_path, data: { turbo_method: :delete }) %></li>
18
+ </ul>
19
+ </li>
20
+ </ul>
12
21
  <%= navigation_menu_with(menu: Koi::Menu.admin_menu(self)) %>
13
22
  </nav>
14
23
  <%= render "layouts/koi/navigation_collapse" %>
@@ -9,9 +9,8 @@ en:
9
9
  admin: "%e %B %Y"
10
10
  koi:
11
11
  auth:
12
- login: "You have been logged in"
13
- logout: "You have been logged out"
14
12
  token_invalid: "Token invalid or consumed already"
13
+ token_consumed: "Please create a password or passkey"
15
14
  labels:
16
15
  new: New
17
16
  search: Search
data/config/routes.rb CHANGED
@@ -3,22 +3,19 @@
3
3
  Rails.application.routes.draw do
4
4
  namespace :admin do
5
5
  resource :session, only: %i[new create destroy] do
6
- post :accept, to: "tokens#update"
6
+ # JWT tokens contain periods
7
+ resources :tokens, param: :token, only: %i[show update], token: /[^\/]+/
7
8
  end
8
9
 
9
10
  resources :url_rewrites
10
11
  resources :admin_users do
11
12
  resources :credentials, only: %i[new create destroy]
12
- post :invite, on: :member, to: "tokens#create"
13
+ resources :tokens, only: %i[create]
13
14
  get :archived, on: :collection
14
15
  put :archive, on: :collection
15
16
  put :restore, on: :collection
16
17
  end
17
18
 
18
- # JWT tokens have dots(represents the 3 parts of data) in them, so we need to allow them in the URL
19
- # can by pass if we use token as a query param
20
- get "token/:token", to: "tokens#show", as: :token, token: /[^\/]+/
21
-
22
19
  resource :cache, only: %i[destroy]
23
20
  resource :dashboard, only: %i[show]
24
21
 
@@ -26,10 +23,8 @@ Rails.application.routes.draw do
26
23
  end
27
24
 
28
25
  scope :admin do
29
- constraints ->(req) { req.session[:admin_user_id].present? } do
30
- mount Katalyst::Content::Engine, at: "content"
31
- mount Katalyst::Navigation::Engine, at: "navigation"
32
- mount Flipper::UI.app(Flipper) => "flipper" if Object.const_defined?("Flipper::UI")
33
- end
26
+ mount Katalyst::Content::Engine, at: "content"
27
+ mount Katalyst::Navigation::Engine, at: "navigation"
28
+ mount Flipper::UI.app(Flipper) => "flipper" if Object.const_defined?("Flipper::UI")
34
29
  end
35
30
  end
data/lib/koi/engine.rb CHANGED
@@ -58,6 +58,7 @@ module Koi
58
58
  end
59
59
 
60
60
  initializer "koi.middleware" do |app|
61
+ app.middleware.use Koi::Middleware::AdminAuthentication
61
62
  app.middleware.use ::ActionDispatch::Static, root.join("public").to_s
62
63
  app.middleware.insert_before Rack::Sendfile, Koi::Middleware::UrlRedirect
63
64
  end
data/lib/koi/menu.rb CHANGED
@@ -17,7 +17,6 @@ module Koi
17
17
  b.add_link(title: "View site", url: "/", target: :blank)
18
18
  b.add_link(title: "Dashboard", url: context.main_app.admin_dashboard_path)
19
19
  b.add_items(priority)
20
- b.add_button(title: "Logout", url: context.main_app.admin_session_path, http_method: :delete)
21
20
  end
22
21
  builder.add_menu(title: "Modules") do |b|
23
22
  b.add_items(modules)
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koi
4
+ module Middleware
5
+ class AdminAuthentication
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ if env["PATH_INFO"].starts_with?("/admin")
12
+ admin_call(env)
13
+ else
14
+ @app.call(env)
15
+ end
16
+ end
17
+
18
+ def admin_call(env)
19
+ request = ActionDispatch::Request.new(env)
20
+ session = ActionDispatch::Request::Session.find(request)
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
28
+
29
+ [303, { "Location" => "/admin/session/new" }, []]
30
+ else
31
+ @app.call(env)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def requires_authentication?(request)
38
+ !request.path.starts_with?("/admin/session")
39
+ end
40
+
41
+ def authenticated?(session)
42
+ session[:admin_user_id].present?
43
+ end
44
+ end
45
+ end
46
+ end