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.
- checksums.yaml +4 -4
- data/app/assets/builds/koi/admin.css +1 -1
- data/app/assets/javascripts/koi/controllers/webauthn_registration_controller.js +15 -7
- data/app/assets/stylesheets/koi/base/_flow.scss +8 -0
- data/app/assets/stylesheets/koi/base/_index.scss +2 -0
- data/app/assets/stylesheets/koi/base/_repel.scss +23 -0
- data/app/controllers/admin/admin_users_controller.rb +2 -0
- data/app/controllers/admin/credentials_controller.rb +5 -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/has_webauthn.rb +2 -1
- data/app/controllers/concerns/koi/controller/is_admin_controller.rb +6 -5
- data/app/views/admin/admin_users/_fields.html+self.erb +3 -0
- data/app/views/admin/admin_users/_fields.html.erb +0 -1
- data/app/views/admin/admin_users/index.html.erb +3 -0
- data/app/views/admin/admin_users/show.html+self.erb +19 -0
- data/app/views/admin/admin_users/show.html.erb +9 -13
- data/app/views/admin/credentials/_credentials.html+self.erb +10 -0
- data/app/views/admin/credentials/_credentials.html.erb +4 -5
- data/app/views/admin/credentials/new.html.erb +27 -4
- 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/_navigation.html.erb +9 -0
- data/config/locales/koi.en.yml +1 -2
- data/config/routes.rb +6 -11
- data/lib/koi/engine.rb +1 -0
- data/lib/koi/menu.rb +0 -1
- data/lib/koi/middleware/admin_authentication.rb +46 -0
- 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 = {
|
10
|
-
|
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)
|
21
|
-
this.responseTarget.value = JSON.stringify(response);
|
22
|
+
async createCredential() {
|
23
|
+
const response = await create(this.options);
|
22
24
|
|
23
|
-
|
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,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
|
+
}
|
@@ -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
|
-
|
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
|
-
|
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(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
|
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)
|
@@ -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
|
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
|
53
|
-
|
53
|
+
def authenticate_local_admins?
|
54
|
+
IsAdminController.authenticate_local_admins
|
54
55
|
end
|
55
56
|
|
56
57
|
def turbo_frame_layout
|
@@ -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.
|
9
|
-
<%= builder.
|
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",
|
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.
|
4
|
-
|
5
|
-
|
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: "
|
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
|
-
|
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 |
|
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,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" %>
|
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
|
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
|