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