katalyst-koi 5.3.0 → 5.4.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/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/components/koi/tables/cells/attachment_component.rb +1 -1
- data/app/controllers/admin/admin_users_controller.rb +2 -2
- data/app/controllers/admin/credentials_controller.rb +32 -51
- data/app/controllers/admin/otps_controller.rb +5 -17
- data/app/controllers/admin/profiles_controller.rb +31 -0
- data/app/controllers/admin/sessions_controller.rb +9 -3
- data/app/controllers/admin/tokens_controller.rb +6 -2
- data/app/controllers/concerns/koi/controller/has_webauthn.rb +53 -9
- data/app/javascript/koi/controllers/webauthn_authentication_controller.js +1 -1
- data/app/javascript/koi/controllers/webauthn_registration_controller.js +8 -18
- data/app/models/admin/credential.rb +4 -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/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 +14 -9
- data/lib/generators/koi/helpers/resource_helpers.rb +1 -1
- data/lib/koi/config.rb +2 -1
- data/lib/koi/engine.rb +1 -0
- metadata +21 -9
- 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
|
@@ -6,7 +6,7 @@ module Koi
|
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
|
|
8
8
|
included do
|
|
9
|
-
|
|
9
|
+
helper Helper
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def webauthn_relying_party
|
|
@@ -17,20 +17,41 @@ module Koi
|
|
|
17
17
|
)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
module Helper
|
|
21
|
+
def webauthn_authentication_options_value
|
|
22
|
+
options = controller.webauthn_relying_party.options_for_authentication
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
session[:authentication_challenge] = options.challenge
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
options
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def webauthn_registration_options_value
|
|
30
|
+
user = current_admin_user.tap do |u|
|
|
31
|
+
u.update!(webauthn_id: WebAuthn.generate_user_id) unless u.webauthn_id
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
options = controller.webauthn_relying_party.options_for_registration(
|
|
35
|
+
user: {
|
|
36
|
+
id: user.webauthn_id,
|
|
37
|
+
name: user.email,
|
|
38
|
+
display_name: user.name,
|
|
39
|
+
},
|
|
40
|
+
exclude: user.credentials.pluck(:external_id),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
session[:registration_challenge] = options.challenge
|
|
44
|
+
|
|
45
|
+
options
|
|
46
|
+
end
|
|
26
47
|
end
|
|
27
48
|
|
|
28
|
-
def webauthn_authenticate!
|
|
29
|
-
return if
|
|
49
|
+
def webauthn_authenticate!(response)
|
|
50
|
+
return if response.blank?
|
|
30
51
|
|
|
31
52
|
webauthn_credential, stored_credential = webauthn_relying_party.verify_authentication(
|
|
32
|
-
JSON.parse(
|
|
33
|
-
session
|
|
53
|
+
JSON.parse(response),
|
|
54
|
+
session.delete(:authentication_challenge),
|
|
34
55
|
) do |credential|
|
|
35
56
|
Admin::Credential.find_by!(external_id: credential.id)
|
|
36
57
|
end
|
|
@@ -44,6 +65,29 @@ module Koi
|
|
|
44
65
|
rescue ActiveRecord::RecordNotFound, WebAuthn::VerificationError
|
|
45
66
|
false
|
|
46
67
|
end
|
|
68
|
+
|
|
69
|
+
def webauthn_register!(response)
|
|
70
|
+
return if response.blank?
|
|
71
|
+
|
|
72
|
+
webauthn_credential = webauthn_relying_party.verify_registration(
|
|
73
|
+
JSON.parse(response),
|
|
74
|
+
session.delete(:registration_challenge),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
current_admin_user
|
|
78
|
+
.credentials
|
|
79
|
+
.create_with(nickname: webauthn_nickname,
|
|
80
|
+
public_key: webauthn_credential.public_key,
|
|
81
|
+
sign_count: webauthn_credential.sign_count)
|
|
82
|
+
.create_or_find_by!(
|
|
83
|
+
external_id: webauthn_credential.id,
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def webauthn_nickname
|
|
88
|
+
user_agent = UserAgent.parse(request.user_agent)
|
|
89
|
+
"#{user_agent.browser} (#{user_agent.platform})"
|
|
90
|
+
end
|
|
47
91
|
end
|
|
48
92
|
end
|
|
49
93
|
end
|
|
@@ -1,39 +1,29 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
|
2
2
|
|
|
3
3
|
export default class WebauthnRegistrationController extends Controller {
|
|
4
|
+
static targets = ["response"];
|
|
4
5
|
static values = {
|
|
5
6
|
options: Object,
|
|
6
|
-
response: Object,
|
|
7
7
|
};
|
|
8
|
-
static targets = ["intro", "nickname", "response"];
|
|
9
8
|
|
|
10
9
|
submit(e) {
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
) {
|
|
15
|
-
e.
|
|
16
|
-
|
|
17
|
-
}
|
|
10
|
+
if (this.responseTarget.value) return;
|
|
11
|
+
|
|
12
|
+
e.preventDefault();
|
|
13
|
+
this.createCredential().then(() => {
|
|
14
|
+
e.target.submit();
|
|
15
|
+
});
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
async createCredential() {
|
|
21
19
|
const credential = await navigator.credentials.create(this.options);
|
|
22
|
-
|
|
23
|
-
this.responseValue = credential.toJSON();
|
|
24
20
|
this.responseTarget.value = JSON.stringify(credential.toJSON());
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
responseValueChanged(response) {
|
|
28
|
-
const responsePresent = response !== "";
|
|
29
|
-
this.introTarget.toggleAttribute("hidden", responsePresent);
|
|
30
|
-
this.nicknameTarget.toggleAttribute("hidden", !responsePresent);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
23
|
get options() {
|
|
34
24
|
return {
|
|
35
25
|
publicKey: PublicKeyCredential.parseCreationOptionsFromJSON(
|
|
36
|
-
this.optionsValue
|
|
26
|
+
this.optionsValue,
|
|
37
27
|
),
|
|
38
28
|
};
|
|
39
29
|
}
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
<h1><%= admin_user %></h1>
|
|
9
9
|
|
|
10
10
|
<%= actions_list do %>
|
|
11
|
+
<% if admin_user == current_admin %>
|
|
12
|
+
<li><%= link_to("Profile", admin_profile_path) %></li>
|
|
13
|
+
<% end %>
|
|
11
14
|
<li><%= link_to("Edit", edit_admin_admin_user_path(admin_user)) %></li>
|
|
12
15
|
<% end %>
|
|
13
16
|
<% end %>
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
<%# locals: (admin_user:) %>
|
|
1
|
+
<%# locals: (admin_user:, collection: admin_user.credentials) %>
|
|
2
2
|
|
|
3
|
-
<%= table_with(id: dom_id(admin_user, :credentials), collection:
|
|
4
|
-
<%
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
<%= table_with(id: dom_id(admin_user, :credentials), collection:) do |row, credential| %>
|
|
4
|
+
<% row.text(:nickname, label: "Name") do |cell| %>
|
|
5
|
+
<%= link_to(cell, admin_credential_path(credential)) %>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% row.date(:created_at, label: "Created") %>
|
|
8
|
+
<% row.date(:updated_at, label: "Last use") do |date| %>
|
|
9
|
+
<%= date unless credential.created_at == credential.updated_at %>
|
|
7
10
|
<% end %>
|
|
8
11
|
<% end %>
|
|
@@ -1,43 +1,34 @@
|
|
|
1
|
-
<%# locals: (admin_user
|
|
1
|
+
<%# locals: (admin_user:) %>
|
|
2
2
|
|
|
3
|
-
<%=
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<%= form.govuk_text_field :nickname, label: { text: "Passkey name" } do %>
|
|
35
|
-
Enter a name for this passkey to help you distinguish it from other passkeys you may have for this site.
|
|
36
|
-
<br>
|
|
37
|
-
Example: My Phone, Chrome, iCloud, 1Password
|
|
38
|
-
<% end %>
|
|
39
|
-
</section>
|
|
40
|
-
<% end %>
|
|
41
|
-
</main>
|
|
42
|
-
<%= koi_modal_footer("Next", nil, form_id: dom_id(credential, :form)) %>
|
|
3
|
+
<%= content_for(:roadblock) do %>
|
|
4
|
+
<header>
|
|
5
|
+
<icon aria-hidden="true" class="icon" data-icon="fingerprint"></icon>
|
|
6
|
+
<h1>Create a passkey</h1>
|
|
7
|
+
</header>
|
|
8
|
+
<p>
|
|
9
|
+
Passkeys are secure secrets that are stored by your device.
|
|
10
|
+
You will need the device where your passkey is stored to log in.
|
|
11
|
+
</p>
|
|
12
|
+
<p>
|
|
13
|
+
Unlike a password, your password doesn't get sent to the server when you log
|
|
14
|
+
in and can't be stolen in a data breach. When you log in with a passkey,
|
|
15
|
+
your operating system will prompt you for permission to use the passkey
|
|
16
|
+
secret to authenticate the login attempt.
|
|
17
|
+
</p>
|
|
18
|
+
<p>
|
|
19
|
+
We recommend that you store your passkey on your phone or cloud account.
|
|
20
|
+
Depending on your browser, you may need to choose "more options" to see
|
|
21
|
+
a QR code that you can scan with your phone.
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<%= form_with(model: Admin::Credential.new,
|
|
25
|
+
url: admin_profile_credentials_path,
|
|
26
|
+
data: {
|
|
27
|
+
controller: "webauthn-registration",
|
|
28
|
+
action: "submit->webauthn-registration#submit",
|
|
29
|
+
webauthn_registration_options_value:,
|
|
30
|
+
}) do |form| %>
|
|
31
|
+
<%= form.hidden_field :response, data: { webauthn_registration_target: "response" } %>
|
|
32
|
+
<%= form.button("Next", type: :submit, class: "button") %>
|
|
33
|
+
<% end %>
|
|
43
34
|
<% end %>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<%# locals: (credential:) %>
|
|
2
|
+
|
|
3
|
+
<% content_for(:header) do %>
|
|
4
|
+
<%= breadcrumb_list do %>
|
|
5
|
+
<li><%= link_to(credential.admin, admin_profile_path) %></li>
|
|
6
|
+
<% end %>
|
|
7
|
+
|
|
8
|
+
<h1>Passkey</h1>
|
|
9
|
+
|
|
10
|
+
<%= actions_list do %>
|
|
11
|
+
<li><%= link_to_delete(credential, url: admin_credential_path(credential)) %></li>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<%= form_with(model: credential, url: admin_credential_path(credential)) do |form| %>
|
|
16
|
+
<%= form.govuk_text_field(:nickname) %>
|
|
17
|
+
|
|
18
|
+
<%= form.button(type: :submit, class: "button") %>
|
|
19
|
+
<% end %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<%# locals: (admin_user:) %>
|
|
2
2
|
|
|
3
|
-
<%= form_with(model: admin_user) do |form| %>
|
|
3
|
+
<%= form_with(model: admin_user, url: admin_profile_path) do |form| %>
|
|
4
4
|
<%= form.govuk_text_field :email %>
|
|
5
5
|
<%= form.govuk_text_field :name %>
|
|
6
6
|
<%= form.govuk_password_field :password, label: { text: "Password (optional)" } %>
|
|
@@ -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 current_admin_user %>
|
|
18
|
+
<ul class="navigation-group | flow" role="list">
|
|
19
|
+
<li>
|
|
20
|
+
<span><%= 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,29 @@
|
|
|
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
|
+
resource :profile, only: %i[show edit update], shallow: true do
|
|
17
|
+
resources :credentials, only: %i[show new create update destroy]
|
|
18
|
+
resource :otp, only: %i[new create destroy]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
resource :session, only: %i[new create destroy] do
|
|
22
|
+
# JWT tokens contain periods
|
|
23
|
+
resources :tokens, param: :token, only: %i[show update], token: /[^\/]+/
|
|
24
|
+
end
|
|
25
|
+
|
|
22
26
|
resources :url_rewrites
|
|
27
|
+
resources :well_knowns
|
|
23
28
|
|
|
24
29
|
root to: redirect("admin/dashboard")
|
|
25
30
|
end
|