katalyst-koi 4.13.0 → 4.14.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/assets/builds/koi/admin.css +1 -1
- data/app/controllers/admin/sessions_controller.rb +12 -7
- data/app/controllers/admin/tokens_controller.rb +28 -23
- data/app/controllers/concerns/koi/controller/is_admin_controller.rb +6 -5
- data/app/views/admin/admin_users/show.html.erb +1 -1
- data/app/views/admin/sessions/new.html.erb +6 -3
- data/app/views/admin/tokens/create.turbo_stream.erb +1 -1
- data/app/views/admin/tokens/show.html.erb +1 -2
- data/app/views/layouts/koi/application.html.erb +1 -0
- data/app/views/layouts/koi/frame.html.erb +4 -0
- data/config/locales/koi.en.yml +1 -2
- data/config/routes.rb +6 -11
- data/lib/generators/koi/admin_controller/templates/controller_spec.rb.tt +5 -0
- data/lib/koi/engine.rb +1 -0
- data/lib/koi/middleware/admin_authentication.rb +46 -0
- metadata +4 -3
@@ -4,14 +4,13 @@ module Admin
|
|
4
4
|
class SessionsController < ApplicationController
|
5
5
|
include Koi::Controller::HasWebauthn
|
6
6
|
|
7
|
-
|
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
|
-
|
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,
|
22
|
+
redirect_to(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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
49
|
-
|
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)
|
@@ -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
|
53
|
-
|
53
|
+
def authenticate_local_admins?
|
54
|
+
IsAdminController.authenticate_local_admins
|
54
55
|
end
|
55
56
|
|
56
57
|
def turbo_frame_layout
|
@@ -17,7 +17,7 @@
|
|
17
17
|
method: :delete,
|
18
18
|
form: { data: { turbo_confirm: "Are you sure?" } } %>
|
19
19
|
<% end %>
|
20
|
-
<%= button_to "Generate login link",
|
20
|
+
<%= button_to "Generate login link", admin_admin_user_tokens_path(admin), class: "button button--primary", form: { id: "invite" } %>
|
21
21
|
</div>
|
22
22
|
|
23
23
|
<h2>Authentication</h2>
|
@@ -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 |
|
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
|
-
|
21
|
-
<%= f.
|
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,
|
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:
|
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,7 @@
|
|
9
9
|
|
10
10
|
<!-- META -->
|
11
11
|
<%= csrf_meta_tags %>
|
12
|
+
<%= csp_meta_tag %>
|
12
13
|
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=0.6667">
|
13
14
|
<meta name="robots" content="noindex,nofollow">
|
14
15
|
<%= Koi::Release.meta_tags(self) %>
|
@@ -1,5 +1,9 @@
|
|
1
1
|
<html>
|
2
2
|
<head>
|
3
|
+
<%# Workaround for https://github.com/hotwired/turbo-rails/issues/669 %>
|
4
|
+
<%= csrf_meta_tags %>
|
5
|
+
<%= csp_meta_tag %>
|
6
|
+
|
3
7
|
<%# include all turbo-track elements so turbo knows it can cache frame visits %>
|
4
8
|
<%= stylesheet_link_tag Koi.config.admin_stylesheet, "data-turbo-track": "reload" %>
|
5
9
|
<%= javascript_importmap_tags "koi/admin" %>
|
data/config/locales/koi.en.yml
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
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
|
@@ -124,6 +124,7 @@ RSpec.describe <%= controller_class_name %>Controller do
|
|
124
124
|
|
125
125
|
describe "DELETE /admin/<%= plural_name %>/:id" do
|
126
126
|
let(:action) { delete polymorphic_path([:admin, model]) }
|
127
|
+
let!(:model) { create(:<%= singular_name %>) }
|
127
128
|
|
128
129
|
it_behaves_like "requires admin"
|
129
130
|
|
@@ -131,5 +132,9 @@ RSpec.describe <%= controller_class_name %>Controller do
|
|
131
132
|
action
|
132
133
|
expect(response).to redirect_to(polymorphic_path([:admin, <%= class_name %>]))
|
133
134
|
end
|
135
|
+
|
136
|
+
it "deletes the <%= singular_name %>" do
|
137
|
+
expect { action }.to change(<%= class_name %>, :count).by(-1)
|
138
|
+
end
|
134
139
|
end
|
135
140
|
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
|
@@ -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
|
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.14.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-11-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -457,6 +457,7 @@ files:
|
|
457
457
|
- lib/koi/form_builder.rb
|
458
458
|
- lib/koi/menu.rb
|
459
459
|
- lib/koi/menu/builder.rb
|
460
|
+
- lib/koi/middleware/admin_authentication.rb
|
460
461
|
- lib/koi/middleware/url_redirect.rb
|
461
462
|
- lib/koi/release.rb
|
462
463
|
- spec/factories/admins.rb
|
@@ -481,7 +482,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
481
482
|
- !ruby/object:Gem::Version
|
482
483
|
version: '0'
|
483
484
|
requirements: []
|
484
|
-
rubygems_version: 3.5.
|
485
|
+
rubygems_version: 3.5.22
|
485
486
|
signing_key:
|
486
487
|
specification_version: 4
|
487
488
|
summary: Koi CMS admin framework
|