katalyst-koi 4.14.0 → 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.
@@ -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
@@ -19,7 +19,7 @@ module Admin
19
19
 
20
20
  session[:admin_user_id] = admin_user.id
21
21
 
22
- redirect_to(params[:redirect].presence || admin_dashboard_path, status: :see_other)
22
+ redirect_to(url_from(params[:redirect].presence) || admin_dashboard_path, status: :see_other)
23
23
  else
24
24
  admin_user = Admin::User.new(session_params.slice(:email, :password))
25
25
  admin_user.errors.add(:email, "Invalid email or password")
@@ -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
@@ -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),
@@ -19,13 +25,3 @@
19
25
  <% end %>
20
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 %>
@@ -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/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)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: katalyst-koi
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.14.0
4
+ version: 4.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katalyst Interactive
@@ -269,11 +269,13 @@ files:
269
269
  - app/assets/javascripts/koi/utils/transition.js
270
270
  - app/assets/stylesheets/koi/admin.scss
271
271
  - app/assets/stylesheets/koi/base/_button.scss
272
+ - app/assets/stylesheets/koi/base/_flow.scss
272
273
  - app/assets/stylesheets/koi/base/_icon.scss
273
274
  - app/assets/stylesheets/koi/base/_index.scss
274
275
  - app/assets/stylesheets/koi/base/_input.scss
275
276
  - app/assets/stylesheets/koi/base/_link.scss
276
277
  - app/assets/stylesheets/koi/base/_list.scss
278
+ - app/assets/stylesheets/koi/base/_repel.scss
277
279
  - app/assets/stylesheets/koi/base/_tables.scss
278
280
  - app/assets/stylesheets/koi/base/_typography.scss
279
281
  - app/assets/stylesheets/koi/components/_actions-group.scss
@@ -362,12 +364,15 @@ files:
362
364
  - app/models/application_record.rb
363
365
  - app/models/concerns/koi/model/archivable.rb
364
366
  - app/models/url_rewrite.rb
367
+ - app/views/admin/admin_users/_fields.html+self.erb
365
368
  - app/views/admin/admin_users/_fields.html.erb
366
369
  - app/views/admin/admin_users/archived.html.erb
367
370
  - app/views/admin/admin_users/edit.html.erb
368
371
  - app/views/admin/admin_users/index.html.erb
369
372
  - app/views/admin/admin_users/new.html.erb
373
+ - app/views/admin/admin_users/show.html+self.erb
370
374
  - app/views/admin/admin_users/show.html.erb
375
+ - app/views/admin/credentials/_credentials.html+self.erb
371
376
  - app/views/admin/credentials/_credentials.html.erb
372
377
  - app/views/admin/credentials/create.turbo_stream.erb
373
378
  - app/views/admin/credentials/destroy.turbo_stream.erb