Skip to content

Commit

Permalink
Convert API key form to stimulus, add exclusive checkbox (rubygems#4418)
Browse files Browse the repository at this point in the history
* The form now helps you with exclusive scopes and auto-deselects
  the other checkboxes when exclusive scopes are chosen.
* Convert api key form to partial and add missing translations keys
  • Loading branch information
martinemde committed Feb 9, 2024
1 parent 4f94ab4 commit 7e9a0d3
Show file tree
Hide file tree
Showing 21 changed files with 349 additions and 194 deletions.
12 changes: 12 additions & 0 deletions app/helpers/api_keys_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ def gem_scope(api_key)
api_key.rubygem ? api_key.rubygem.name : t("api_keys.all_gems")
end

def api_key_checkbox(form, api_scope)
exclusive = ApiKey::EXCLUSIVE_SCOPES.include?(api_scope)
gem_scope = ApiKey::APPLICABLE_GEM_API_SCOPES.include?(api_scope)

data = {}
data[:exclusive_checkbox_target] = exclusive ? "exclusive" : "inclusive"
data[:gem_scope_target] = "checkbox" if gem_scope

html_options = { class: "form__checkbox__input", id: api_scope, data: }
form.check_box api_scope, html_options, "true", "false"
end

private

def invalid_gem_tooltip(name)
Expand Down
1 change: 0 additions & 1 deletion app/javascript/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Rails from "@rails/ujs";
Rails.start();
import "controllers"

import "src/api_key_form";
import "src/autocomplete";
import "src/clipboard_buttons";
import "src/multifactor_auths";
Expand Down
35 changes: 35 additions & 0 deletions app/javascript/controllers/exclusive_checkbox_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Controller } from "@hotwired/stimulus"

// Supports one exclusive target, but many inclusive targets
export default class extends Controller {
static targets = ["inclusive", "exclusive"]

connect() {
// Unselect all inclusive targets if the exclusive target is selected on load
this.updateExclusive()
}

exclusiveTargetConnected(el) {
el.addEventListener("change", () => this.updateExclusive())
}

inclusiveTargetConnected(el) {
el.addEventListener("change", (e) => {
if (e.currentTarget.checked) { this.uncheck(this.exclusiveTarget) }
})
}

updateExclusive() {
if (this.exclusiveTarget.checked) {
this.inclusiveTargets.forEach(this.uncheck)
}
}

uncheck(checkbox) {
if (checkbox.checked) {
checkbox.checked = false
checkbox.dispatchEvent(new Event("change"))
}
}
}

43 changes: 43 additions & 0 deletions app/javascript/controllers/gem_scope_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
static targets = ["checkbox", "selector"]

connect() {
this.toggleSelector()
}

checkboxTargetConnected(el) {
el.addEventListener("change", () => this.toggleSelector())
}

toggleSelector() {
const selected = this.checkboxTargets.find((target) => target.checked)

if (selected) {
this.selectorTarget.disabled = false;
this.removeHiddenRubygemField();
} else {
this.selectorTarget.value = "";
this.selectorTarget.disabled = true;
this.addHiddenRubygemField();
}
}

addHiddenRubygemField() {
if (this.hiddenField) { return }
this.hiddenField = document.createElement("input");
this.hiddenField.type = "hidden";
this.hiddenField.name = "api_key[rubygem_id]";
this.hiddenField.value = "";
this.element.appendChild(this.hiddenField);
}

removeHiddenRubygemField() {
if (this.hiddenField) {
this.hiddenField.remove();
this.hiddenField = null;
}
}
}

38 changes: 0 additions & 38 deletions app/javascript/src/api_key_form.js

This file was deleted.

3 changes: 2 additions & 1 deletion app/models/api_key.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class ApiKey < ApplicationRecord
API_SCOPES = %i[index_rubygems push_rubygem yank_rubygem add_owner remove_owner access_webhooks show_dashboard].freeze
API_SCOPES = %i[show_dashboard index_rubygems push_rubygem yank_rubygem add_owner remove_owner access_webhooks].freeze
APPLICABLE_GEM_API_SCOPES = %i[push_rubygem yank_rubygem add_owner remove_owner].freeze
EXCLUSIVE_SCOPES = %i[show_dashboard].freeze

belongs_to :owner, polymorphic: true

Expand Down
41 changes: 41 additions & 0 deletions app/views/api_keys/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<%= form_with model: [:profile, @api_key], data: { controller: "exclusive-checkbox gem-scope" } do |f| %>
<%= label_tag :name, t("api_keys.index.name"), class: "form__label" %>
<%= f.text_field :name, class: "form__input", autocomplete: :off %>

<div class="form__group">
<%= label_tag :scope, t(".exclusive_scopes"), class: "form__label" %>
<% ApiKey::EXCLUSIVE_SCOPES.each do |api_scope| %>
<div class = "form__checkbox__item">
<%= api_key_checkbox(f, api_scope) %>
<%= label_tag api_scope, t("api_keys.index.#{api_scope}"), class: "form__checkbox__label" %>
</div>
<% end %>
</div>
<div class="form__group">
<%= label_tag :scope, t("api_keys.index.scopes"), class: "form__label" %>
<% (ApiKey::API_SCOPES - ApiKey::EXCLUSIVE_SCOPES).each do |api_scope| %>
<div class = "form__checkbox__item">
<%= api_key_checkbox(f, api_scope) %>
<%= label_tag api_scope, t("api_keys.index.#{api_scope}"), class: "form__checkbox__label" %>
</div>
<% end %>
</div>

<div class="form__group api_key_rubygem_id_form">
<%= label_tag :rubygem_id, t(".rubygem_scope"), class: "form__label" %>
<p><%= t(".rubygem_scope_info") %></p>
<%= f.collection_select :rubygem_id, current_user.rubygems.by_name, :id, :name, { include_blank: t("api_keys.all_gems") }, selected: :rubygem_id, class: "form__input form__select", data: { gem_scope_target: "selector" } %>
</div>

<% unless current_user.mfa_disabled? || current_user.mfa_ui_and_api? %>
<div class="form__group">
<%= label_tag :mfa, t(".multifactor_auth"), class: "form__label" %>
<div class="form__checkbox__item">
<%= f.check_box :mfa, { class: "form__checkbox__input", id: :mfa, checked: true } , "true", "false" %>
<%= label_tag :mfa, t(".enable_mfa"), class: "form__checkbox__label" %>
</div>
</div>
<% end %>
<%= f.submit class: "form__submit" %>
<% end %>
33 changes: 1 addition & 32 deletions app/views/api_keys/edit.html.erb
Original file line number Diff line number Diff line change
@@ -1,36 +1,5 @@
<% @title = t(".edit_api_key") %>

<div class="t-body">
<%= form_for @api_key, url: profile_api_key_path do |f| %>
<%= label_tag :name, t("api_keys.index.name"), class: "form__label" %>
<%= f.text_field :name, class: "form__input", autocomplete: :off %>

<div class="form__group">
<%= label_tag :scope, t("api_keys.index.scopes"), class: "form__label" %>
<% ApiKey::API_SCOPES.each do |api_scope| %>
<div class = "form__checkbox__item">
<%= f.check_box "#{api_scope}", { class: "form__checkbox__input", id: "#{api_scope}" } , "true", "false" %>
<%= label_tag api_scope, t("api_keys.index.#{api_scope}"), class: "form__checkbox__label" %>
</div>
<% end %>
</div>

<div class="form__group api_key_rubygem_id_form">
<%= label_tag :rubygem_id, t(".rubygem_scope"), class: "form__label" %>
<p><%= t(".rubygem_scope_info") %></p>
<%= f.collection_select :rubygem_id, current_user.rubygems.by_name, :id, :name, { include_blank: t("api_keys.all_gems") }, selected: :rubygem_id, class: "form__input form__select" %>
</div>

<% unless current_user.mfa_disabled? || current_user.mfa_ui_and_api? %>
<div class="form__group">
<%= label_tag :mfa, t(".multifactor_auth"), class: "form__label" %>
<div class="form__checkbox__item">
<%= f.check_box :mfa, { class: "form__checkbox__input", id: :mfa } , "true", "false" %>
<%= label_tag :mfa, t(".enable_mfa"), class: "form__checkbox__label" %>
</div>
</div>
<% end %>
<%= f.submit "Update", class: "form__submit" %>
<% end %>
<%= render "form", api_key: @api_key %>
</div>
33 changes: 1 addition & 32 deletions app/views/api_keys/new.html.erb
Original file line number Diff line number Diff line change
@@ -1,36 +1,5 @@
<% @title = t(".new_api_key") %>

<div class="t-body">
<%= form_for @api_key, url: profile_api_keys_path do |f| %>
<%= label_tag :name, t("api_keys.index.name"), class: "form__label" %>
<%= f.text_field :name, class: "form__input", autocomplete: :off %>

<div class="form__group">
<%= label_tag :scope, t("api_keys.index.scopes"), class: "form__label" %>
<% ApiKey::API_SCOPES.each do |api_scope| %>
<div class = "form__checkbox__item">
<%= f.check_box "#{api_scope}", { class: "form__checkbox__input", id: "#{api_scope}" } , "true", "false" %>
<%= label_tag api_scope, t("api_keys.index.#{api_scope}"), class: "form__checkbox__label" %>
</div>
<% end %>
</div>

<div class="form__group api_key_rubygem_id_form">
<%= label_tag :rubygem_id, t(".rubygem_scope"), class: "form__label" %>
<p><%= t(".rubygem_scope_info") %></p>
<%= f.collection_select :rubygem_id, current_user.rubygems.by_name, :id, :name, { include_blank: t("api_keys.all_gems") }, selected: :rubygem_id, class: "form__input form__select" %>
</div>

<% unless current_user.mfa_disabled? || current_user.mfa_ui_and_api? %>
<div class="form__group">
<%= label_tag :mfa, t(".multifactor_auth"), class: "form__label" %>
<div class="form__checkbox__item">
<%= f.check_box :mfa, { class: "form__checkbox__input", id: :mfa, checked: true } , "true", "false" %>
<%= label_tag :mfa, t(".enable_mfa"), class: "form__checkbox__label" %>
</div>
</div>
<% end %>
<%= f.submit "Create", class: "form__submit" %>
<% end %>
<%= render "form", api_key: @api_key %>
</div>
4 changes: 2 additions & 2 deletions app/views/oidc/api_key_roles/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
<% end %>
</div>
<div class="form__group">
<%= f.label :gems, t("api_keys.new.rubygem_scope"), class: "form__label" %>
<p><%= t("api_keys.new.rubygem_scope_info") %></p>
<%= f.label :gems, t("api_keys.form.rubygem_scope"), class: "form__label" %>
<p><%= t("api_keys.form.rubygem_scope_info") %></p>
<%= f.collection_select :gems, current_user.rubygems.by_name, :name, :name, { include_blank: t("api_keys.all_gems"), include_hidden: false }, selected: :name, class: "form__input form__select", multiple: true %>
</div>
</div>
Expand Down
22 changes: 14 additions & 8 deletions config/locales/de.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ de:
try_again:
advanced_search:
authenticate:
helpers:
submit:
create:
update:
activerecord:
attributes:
linkset:
Expand Down Expand Up @@ -89,6 +93,10 @@ de:
unavailable:
models:
user:
api_key:
zero:
one:
many:
activemodel:
attributes:
oidc/provider/configuration:
Expand All @@ -103,6 +111,12 @@ de:
gems:
too_long:
api_keys:
form:
exclusive_scopes:
rubygem_scope:
rubygem_scope_info:
multifactor_auth:
enable_mfa:
create:
success:
invalid_gem:
Expand Down Expand Up @@ -132,21 +146,13 @@ de:
mfa:
new:
new_api_key:
multifactor_auth:
enable_mfa:
rubygem_scope:
rubygem_scope_info:
reset:
success:
update:
success:
invalid_gem:
edit:
edit_api_key:
multifactor_auth:
enable_mfa:
rubygem_scope:
rubygem_scope_info:
invalid_key:
all_gems:
gem_ownership_removed:
Expand Down
22 changes: 14 additions & 8 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ en:
try_again: Something went wrong. Please try again.
advanced_search: Advanced Search
authenticate: Authenticate
helpers:
submit:
create: "Create %{model}"
update: "Update %{model}"
activerecord:
attributes:
linkset:
Expand Down Expand Up @@ -99,6 +103,10 @@ en:
unavailable: "is already in use"
models:
user: User
api_key:
zero: API Keys
one: API Key
many: API Keys
activemodel:
attributes:
oidc/provider/configuration:
Expand All @@ -113,6 +121,12 @@ en:
gems:
too_long: "may include at most 1 gem"
api_keys:
form:
exclusive_scopes: Exclusive Scopes
rubygem_scope: Gem Scope
rubygem_scope_info: This scope restricts gem push/yank and owner add/remove commands to a specific gem.
multifactor_auth: Multi-factor authentication
enable_mfa: Enable MFA
create:
success: "Created new API key"
invalid_gem: Selected gem cannot be scoped to this key
Expand Down Expand Up @@ -142,21 +156,13 @@ en:
mfa: MFA
new:
new_api_key: New API key
multifactor_auth: Multi-factor authentication
enable_mfa: Enable MFA
rubygem_scope: Gem Scope
rubygem_scope_info: This scope restricts gem push/yank and owner add/remove commands to a specific gem.
reset:
success: "Deleted all API keys"
update:
success: "Successfully updated API key"
invalid_gem: Selected gem cannot be scoped to this key
edit:
edit_api_key: "Edit API key"
multifactor_auth: Multi-factor authentication
enable_mfa: Enable MFA
rubygem_scope: Gem Scope
rubygem_scope_info: This scope restricts gem push/yank and owner add/remove commands to a specific gem.
invalid_key: An invalid API key cannot be edited. Please delete it and create a new one.
all_gems: All Gems
gem_ownership_removed: Ownership of %{rubygem_name} has been removed after being scoped to this key.
Expand Down
Loading

0 comments on commit 7e9a0d3

Please sign in to comment.