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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/katalyst/koi.esm.js +9 -19
  3. data/app/assets/builds/katalyst/koi.js +9 -19
  4. data/app/assets/builds/katalyst/koi.min.js +1 -1
  5. data/app/assets/builds/katalyst/koi.min.js.map +1 -1
  6. data/app/assets/stylesheets/koi/blocks/index.css +1 -0
  7. data/app/assets/stylesheets/koi/{login.css → blocks/roadblock.css} +8 -5
  8. data/app/components/koi/tables/cells/attachment_component.rb +1 -1
  9. data/app/controllers/admin/admin_users_controller.rb +2 -2
  10. data/app/controllers/admin/credentials_controller.rb +32 -51
  11. data/app/controllers/admin/otps_controller.rb +5 -17
  12. data/app/controllers/admin/profiles_controller.rb +31 -0
  13. data/app/controllers/admin/sessions_controller.rb +9 -3
  14. data/app/controllers/admin/tokens_controller.rb +6 -2
  15. data/app/controllers/concerns/koi/controller/has_webauthn.rb +53 -9
  16. data/app/javascript/koi/controllers/webauthn_authentication_controller.js +1 -1
  17. data/app/javascript/koi/controllers/webauthn_registration_controller.js +8 -18
  18. data/app/models/admin/credential.rb +4 -0
  19. data/app/views/admin/admin_users/edit.html.erb +3 -1
  20. data/app/views/admin/admin_users/show.html.erb +3 -0
  21. data/app/views/admin/credentials/_credentials.html.erb +8 -5
  22. data/app/views/admin/credentials/new.html.erb +32 -41
  23. data/app/views/admin/credentials/show.html.erb +19 -0
  24. data/app/views/admin/otps/_form.html.erb +1 -1
  25. data/app/views/admin/{admin_users/_form.html+self.erb → profiles/_form.html.erb} +1 -1
  26. data/app/views/admin/{admin_users/edit.html+self.erb → profiles/edit.html.erb} +0 -1
  27. data/app/views/admin/{admin_users/show.html+self.erb → profiles/show.html.erb} +15 -10
  28. data/app/views/admin/sessions/new.html.erb +26 -27
  29. data/app/views/admin/sessions/otp.html.erb +13 -5
  30. data/app/views/admin/sessions/password.html.erb +16 -8
  31. data/app/views/admin/tokens/show.html.erb +12 -8
  32. data/app/views/layouts/koi/_application_navigation.html.erb +13 -9
  33. data/app/views/layouts/koi/application.html.erb +19 -10
  34. data/config/locales/koi.en.yml +0 -1
  35. data/config/routes.rb +14 -9
  36. data/lib/generators/koi/helpers/resource_helpers.rb +1 -1
  37. data/lib/koi/config.rb +2 -1
  38. data/lib/koi/engine.rb +1 -0
  39. metadata +21 -9
  40. data/app/views/admin/credentials/_credentials.html+self.erb +0 -12
  41. data/app/views/admin/credentials/create.turbo_stream.erb +0 -5
  42. data/app/views/admin/credentials/destroy.turbo_stream.erb +0 -5
  43. 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
- helper_method :webauthn_auth_options
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
- def webauthn_auth_options
21
- options = webauthn_relying_party.options_for_authentication
20
+ module Helper
21
+ def webauthn_authentication_options_value
22
+ options = controller.webauthn_relying_party.options_for_authentication
22
23
 
23
- session[:authentication_challenge] = options.challenge
24
+ session[:authentication_challenge] = options.challenge
24
25
 
25
- options
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 session_params[:response].blank?
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(session_params[:response]),
33
- session[:authentication_challenge],
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
@@ -17,7 +17,7 @@ export default class WebauthnAuthenticationController extends Controller {
17
17
  get options() {
18
18
  return {
19
19
  publicKey: PublicKeyCredential.parseRequestOptionsFromJSON(
20
- this.optionsValue.publicKey,
20
+ this.optionsValue,
21
21
  ),
22
22
  };
23
23
  }
@@ -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
- this.responseTarget.value === "" &&
13
- e.submitter.formMethod !== "dialog"
14
- ) {
15
- e.preventDefault();
16
- this.createCredential().then();
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.publicKey,
26
+ this.optionsValue,
37
27
  ),
38
28
  };
39
29
  }
@@ -10,5 +10,9 @@ module Admin
10
10
  validates :external_id, uniqueness: true
11
11
  validates :sign_count,
12
12
  numericality: { only_integer: true, greater_than_or_equal_to: 0 }
13
+
14
+ def to_s
15
+ nickname
16
+ end
13
17
  end
14
18
  end
@@ -9,7 +9,9 @@
9
9
  <h1>Edit admin user</h1>
10
10
 
11
11
  <%= actions_list do %>
12
- <li><%= link_to_archive_or_delete(admin_user) %></li>
12
+ <% unless admin_user == current_admin %>
13
+ <li><%= link_to_archive_or_delete(admin_user) %></li>
14
+ <% end %>
13
15
  <% end %>
14
16
  <% end %>
15
17
 
@@ -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: admin_user.credentials) do |t, c| %>
4
- <% t.text :nickname, label: "Name" %>
5
- <% t.date :updated_at, label: "Last use" do |date| %>
6
- <%= date unless c.created_at == c.updated_at %>
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:, credential:, options:) %>
1
+ <%# locals: (admin_user:) %>
2
2
 
3
- <%= koi_modal_tag("edit", title: "New passkey") do %>
4
- <%= koi_modal_header(title: "New passkey", form_id: dom_id(credential, :form)) %>
5
- <main>
6
- <%= form_with(model: credential,
7
- url: admin_admin_user_credentials_path(admin_user),
8
- id: dom_id(credential, :form),
9
- class: "flow prose",
10
- data: {
11
- controller: "webauthn-registration",
12
- action: "submit->webauthn-registration#submit",
13
- webauthn_registration_options_value: { publicKey: options },
14
- }) do |form| %>
15
- <%= form.hidden_field :response, data: { webauthn_registration_target: "response" } %>
16
- <section class="flow prose" data-webauthn-registration-target="intro">
17
- <p>
18
- Passkeys are secure secrets that are stored by your device.
19
- You will need the device where your passkey is stored to log in.
20
- </p>
21
- <p>
22
- Unlike a password, your password doesn't get sent to the server when you log
23
- in and can't be stolen in a data breach. When you log in with a passkey,
24
- your operating system will prompt you for permission to use the passkey
25
- secret to authenticate the login attempt.
26
- </p>
27
- <p>
28
- We recommend that you store your passkey on your phone or cloud account.
29
- Depending on your browser, you may need to choose "more options" to see
30
- a QR code that you can scan with your phone.
31
- </p>
32
- </section>
33
- <section class="flow" data-webauthn-registration-target="nickname" hidden>
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 %>
@@ -2,7 +2,7 @@
2
2
 
3
3
  <%= form_with(id: dom_id(admin_user, :otp),
4
4
  model: admin_user,
5
- url: admin_admin_user_otp_path(admin_user),
5
+ url: admin_profile_otp_path,
6
6
  method: :post,
7
7
  class: "flow") do |form| %>
8
8
  <section class="flow prose">
@@ -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)" } %>
@@ -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 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
- <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,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
- resources :well_knowns
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