diff --git a/app/assets/stylesheets/02_utils.scss b/app/assets/stylesheets/02_utils.scss
index cd36089c7..05a5afdd5 100644
--- a/app/assets/stylesheets/02_utils.scss
+++ b/app/assets/stylesheets/02_utils.scss
@@ -161,6 +161,13 @@
margin-left: auto;
}
+.truncate-80 {
+ width: 80%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
// generate spacer utility like bootstrap my-2 -> margin-left/right: 2 * $default-spacer
// using $direction.key as css modifier, $direction.values to set css properties
// scale it using $steps
diff --git a/app/assets/stylesheets/toggle-switch.scss b/app/assets/stylesheets/toggle-switch.scss
index 38da8d67a..e25a5390d 100644
--- a/app/assets/stylesheets/toggle-switch.scss
+++ b/app/assets/stylesheets/toggle-switch.scss
@@ -9,7 +9,16 @@
height: 24px;
margin: 0;
margin-right: 15px;
- margin-bottom: 40px;
+
+ margin-bottom: $default-fields-spacer;
+
+ &.small-margin {
+ margin-bottom: $default-spacer;
+ }
+
+ &.no-margin {
+ margin-bottom: 0;
+ }
}
// Hide default HTML checkbox
diff --git a/app/components/profile/api_token_card_component/api_token_card_component.html.haml b/app/components/profile/api_token_card_component/api_token_card_component.html.haml
index b820cf0ce..d74738d1c 100644
--- a/app/components/profile/api_token_card_component/api_token_card_component.html.haml
+++ b/app/components/profile/api_token_card_component/api_token_card_component.html.haml
@@ -6,7 +6,10 @@
= render Dsfr::ListComponent.new do |list|
- api_and_packed_tokens.each do |(api_token, packed_token)|
- list.with_item do
- = render Profile::APITokenComponent.new(api_token:, packed_token:)
+ .fr-card.fr-card--horizontal
+ .fr-card__body.width-100
+ .fr-card__content
+ = render Profile::APITokenComponent.new(api_token:, packed_token:)
%br
= button_to "Créer et afficher un nouveau jeton", api_tokens_path, method: :post, class: "fr-btn fr-btn--secondary"
diff --git a/app/components/profile/api_token_component.rb b/app/components/profile/api_token_component.rb
index a584281d9..7b91244eb 100644
--- a/app/components/profile/api_token_component.rb
+++ b/app/components/profile/api_token_component.rb
@@ -3,4 +3,14 @@ class Profile::APITokenComponent < ApplicationComponent
@api_token = api_token
@packed_token = packed_token
end
+
+ private
+
+ def procedures_to_allow_options
+ @api_token.procedures_to_allow.map { ["#{_1.id} – #{_1.libelle}", _1.id] }
+ end
+
+ def procedures_to_allow_select_options
+ { selected: @api_token.procedures_to_allow.first&.id }
+ end
end
diff --git a/app/components/profile/api_token_component/api_token_component.fr.yml b/app/components/profile/api_token_component/api_token_component.fr.yml
new file mode 100644
index 000000000..52bbadc5f
--- /dev/null
+++ b/app/components/profile/api_token_component/api_token_component.fr.yml
@@ -0,0 +1,6 @@
+fr:
+ allowed_full_access_html: Ce jeton a accès à toutes les démarches attachées à votre compte administrateur
+ allowed_procedures_html:
+ zero: Ce jeton n’a accès à aucune démarche
+ one: Ce jeton a accès a une démarche sélectionnée
+ other: Ce jeton a accès a %{count} démarches sélectionnées
diff --git a/app/components/profile/api_token_component/api_token_component.html.haml b/app/components/profile/api_token_component/api_token_component.html.haml
index bf2c51dfd..2ffdfe33c 100644
--- a/app/components/profile/api_token_component/api_token_component.html.haml
+++ b/app/components/profile/api_token_component/api_token_component.html.haml
@@ -1,15 +1,53 @@
-%p
+%h3.fr-card__title
%b= "#{@api_token.name} "
%span.fr-text--sm= @api_token.prefix
-- if @packed_token.present?
- .fr-text--sm{ style: "width: 80%; word-break: break-all;" }
- - button = render Dsfr::CopyButtonComponent.new(text: @packed_token, title: "Copier le jeton dans le presse-papier", success: "Le jeton a été copié dans le presse-papier")
- = "#{@packed_token} #{button}"
+.fr-card__desc
+ - if @packed_token.present?
+ .fr-text--sm{ style: "width: 80%; word-break: break-all;" }
+ - button = render Dsfr::CopyButtonComponent.new(text: @packed_token, title: "Copier le jeton dans le presse-papier", success: "Le jeton a été copié dans le presse-papier")
+ = "#{@packed_token} #{button}"
- %p Pour des raisons de sécurité, il ne sera plus ré-affiché, notez-le bien.
+ %p Pour des raisons de sécurité, il ne sera plus ré-affiché, notez-le bien.
-- else
- %p Pour des raisons de sécurité, nous ne pouvons vous l’afficher que lors de sa création.
+ - else
+ %p Pour des raisons de sécurité, nous ne pouvons vous l’afficher que lors de sa création.
-= button_to "Révoquer le jeton", api_token_path(@api_token), method: :delete, class: "fr-btn fr-btn--secondary", data: { turbo_confirm: "Confirmez-vous la révocation de ce jeton ? Les applications qui l’utilisent actuellement seront bloquées." }
+ - if @api_token.full_access?
+ %p.fr-text--lg
+ = t('.allowed_full_access_html')
+ - else
+ %p.fr-text--lg
+ = t('.allowed_procedures_html', count: @api_token.allowed_procedures.size)
+
+ - if @api_token.allowed_procedures.empty?
+ = button_to "Autoriser l’accès a toutes les démarches", @api_token, method: :patch, params: { api_token: { disallow_procedure_id: '0' } }, class: "fr-btn fr-btn--secondary"
+ - else
+ %ul
+ - @api_token.allowed_procedures.each do |procedure|
+ %li.flex.justify-between.align-center
+ .truncate-80
+ = "#{procedure.id} – #{procedure.libelle}"
+ = button_to "Supprimer", @api_token, method: :patch, params: { api_token: { disallow_procedure_id: procedure.id } }, class: "fr-btn fr-btn--secondary"
+
+.fr-card__end
+ = form_for @api_token, namespace: dom_id(@api_token, :allowed_procedures), html: { class: 'form form-ds-fr-white mb-3', data: { turbo: true } } do |f|
+ = f.label :allowed_procedure_ids do
+ Autoriser l’accès seulement a des démarches choisies
+ - @api_token.allowed_procedures.each do |procedure|
+ = f.hidden_field :allowed_procedure_ids, value: procedure.id, multiple: true, id: dom_id(procedure, :allowed_procedure)
+ .flex.justify-between.align-center{ 'data-turbo-force': true }
+ = f.select :allowed_procedure_ids, procedures_to_allow_options, procedures_to_allow_select_options, { class: 'no-margin width-66 small', name: "api_token[allowed_procedure_ids][]" }
+ = f.button type: :submit, class: "fr-btn fr-btn--secondary" do
+ Ajouter
+
+ = form_for @api_token, namespace: dom_id(@api_token, :write_access), html: { class: 'form form-ds-fr-white mb-3', data: { turbo: true, controller: 'autosubmit' } } do |f|
+ = f.label :write_access do
+ Ce jeton a accès aux démarches
+ %label.toggle-switch.no-margin
+ = f.check_box :write_access, class: 'toggle-switch-checkbox'
+ %span.toggle-switch-control.round
+ %span.toggle-switch-label.on En lecture et écriture
+ %span.toggle-switch-label.off En lecture seule
+
+ = button_to "Révoquer le jeton", api_token_path(@api_token), method: :delete, class: "fr-btn fr-btn--secondary", data: { turbo_confirm: "Confirmez-vous la révocation de ce jeton ? Les applications qui l’utilisent actuellement seront bloquées." }
diff --git a/app/controllers/api_tokens_controller.rb b/app/controllers/api_tokens_controller.rb
index 6e859de4a..7cadd42b5 100644
--- a/app/controllers/api_tokens_controller.rb
+++ b/app/controllers/api_tokens_controller.rb
@@ -12,7 +12,13 @@ class APITokensController < ApplicationController
def update
@api_token = current_administrateur.api_tokens.find(params[:id])
- @api_token.update!(api_token_params)
+
+ disallow_procedure_id = api_token_params.fetch(:disallow_procedure_id, nil)
+ if disallow_procedure_id.present?
+ @api_token.disallow_procedure(disallow_procedure_id.to_i)
+ else
+ @api_token.update!(api_token_params)
+ end
respond_to do |format|
format.turbo_stream { render :index }
@@ -33,6 +39,6 @@ class APITokensController < ApplicationController
private
def api_token_params
- params.require(:api_token).permit(:name)
+ params.require(:api_token).permit(:name, :write_access, :disallow_procedure_id, allowed_procedure_ids: [])
end
end
diff --git a/app/models/api_token.rb b/app/models/api_token.rb
index f40ccc19d..f93e36d8e 100644
--- a/app/models/api_token.rb
+++ b/app/models/api_token.rb
@@ -18,6 +18,8 @@ class APIToken < ApplicationRecord
belongs_to :administrateur, inverse_of: :api_tokens
has_many :procedures, through: :administrateur
+ before_save :check_allowed_procedure_ids_ownership
+
def context
context = { administrateur_id: administrateur_id, write_access: write_access? }
@@ -28,6 +30,30 @@ class APIToken < ApplicationRecord
end
end
+ def full_access?
+ allowed_procedure_ids.nil?
+ end
+
+ def procedures_to_allow
+ procedures.select(:id, :libelle, :path).where.not(id: allowed_procedure_ids || []).order(:libelle)
+ end
+
+ def allowed_procedures
+ if allowed_procedure_ids.present?
+ procedures.select(:id, :libelle, :path).where(id: allowed_procedure_ids).order(:libelle)
+ else
+ []
+ end
+ end
+
+ def disallow_procedure(procedure_id)
+ allowed_procedure_ids = allowed_procedures.map(&:id) - [procedure_id]
+ if allowed_procedure_ids.empty?
+ allowed_procedure_ids = nil
+ end
+ update!(allowed_procedure_ids:)
+ end
+
# Prefix is made of the first 6 characters of the uuid base64 encoded
# it does not leak plain token
def prefix
@@ -84,4 +110,12 @@ class APIToken < ApplicationRecord
-> (api_token) { api_token if BCrypt::Password.new(api_token.encrypted_token) == plain_token }
end
end
+
+ private
+
+ def check_allowed_procedure_ids_ownership
+ if allowed_procedure_ids.present?
+ self.allowed_procedure_ids = allowed_procedures.map(&:id)
+ end
+ end
end