Skip to content
This repository was archived by the owner on Apr 8, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions app/controllers/admin/editor_tokens_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class Admin::EditorTokensController < AdminController
def create
@editor = Editor.find(params[:editor_id])

@editor.editor_tokens.create!(
iat: Time.zone.now.to_i,
exp: 18.months.from_now.to_i
)

success_message(title: 'Jeton éditeur créé avec succès')
redirect_to edit_admin_editor_path(@editor)
end
end
1 change: 1 addition & 0 deletions app/controllers/editor/delegations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class Editor::DelegationsController < EditorController
before_action :ensure_delegations_enabled

def index
@editor_tokens = current_editor.editor_tokens.order(created_at: :desc)
@delegations = current_editor
.editor_delegations
.includes(authorization_request: %i[organization demandeur])
Expand Down
2 changes: 2 additions & 0 deletions app/models/editor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ class Editor < ApplicationRecord
dependent: :nullify
has_many :editor_delegations,
dependent: :destroy
has_many :editor_tokens,
dependent: :destroy

validates :name, presence: true

Expand Down
37 changes: 37 additions & 0 deletions app/models/editor_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
class EditorToken < ApplicationRecord
belongs_to :editor

validates :exp, presence: true

scope :active, -> { where(blacklisted_at: nil).or(where('blacklisted_at > ?', Time.zone.now)).where('exp > ?', Time.zone.now.to_i) }

def rehash
AccessToken.create(jwt_data)
end

def expired?
exp < Time.zone.now.to_i
end

def blacklisted?
blacklisted_at.present? && blacklisted_at < Time.zone.now
end

def active?
!blacklisted? && !expired?
end

private

def jwt_data
{
uid: id,
jti: id,
sub: editor.name,
version: '1.0',
iat: iat,
exp: exp,
editor: true
}
end
end
45 changes: 45 additions & 0 deletions app/views/admin/editors/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,48 @@

<%= f.submit 'Sauvegarder', class: %w[fr-btn] %>
<% end %>

<h2 class="fr-mt-4w">Jetons éditeur</h2>

<% if @editor.editor_tokens.any? %>
<div class="fr-table fr-table--bordered fr-mb-2w">
<div class="fr-table__wrapper">
<div class="fr-table__container">
<div class="fr-table__content">
<table>
<caption>Jetons éditeur</caption>
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Créé le</th>
<th scope="col">Expiration</th>
<th scope="col">Statut</th>
</tr>
</thead>
<tbody>
<% @editor.editor_tokens.order(created_at: :desc).each do |editor_token| %>
<tr>
<td><%= editor_token.id.first(8) %>&hellip;</td>
<td><%= l(editor_token.created_at, format: :short) %></td>
<td><%= friendly_date_from_timestamp(editor_token.exp) %></td>
<td>
<% if editor_token.active? %>
<p class="fr-badge fr-badge--green-emeraude">Actif</p>
<% else %>
<p class="fr-badge fr-badge--pink-tuile">Inactif</p>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
</div>
<% end %>

<%= button_to 'Générer un nouveau jeton éditeur',
admin_editor_editor_tokens_path(@editor),
method: :post,
class: 'fr-btn fr-btn--secondary' %>
48 changes: 48 additions & 0 deletions app/views/editor/delegations/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,3 +1,51 @@
<% if @editor_tokens.any? %>
<div class="fr-table fr-table--bordered fr-mb-4w">
<div class="fr-table__wrapper">
<div class="fr-table__container">
<div class="fr-table__content">
<table>
<caption><%= t('.editor_tokens_title') %></caption>
<thead>
<tr>
<th scope="col"><%= t('.editor_tokens_table.token') %></th>
<th scope="col"><%= t('.editor_tokens_table.status') %></th>
<th scope="col"><%= t('.editor_tokens_table.expiration') %></th>
</tr>
</thead>
<tbody>
<% @editor_tokens.each do |editor_token| %>
<tr data-controller="clipboard">
<td>
<div class="copy-token">
<input class="fr-input" type="text" readonly
data-clipboard-target="source"
value="<%= editor_token.rehash %>"
style="position: absolute; left: -9999px" />
<button class="fr-btn fr-btn--sm fr-icon-clipboard-fill fr-btn--icon-right"
type="button"
data-action="click->clipboard#copy">
<%= t('.editor_tokens_table.copy') %>
</button>
</div>
</td>
<td>
<% if editor_token.active? %>
<p class="fr-badge fr-badge--green-emeraude"><%= t('.status_active') %></p>
<% else %>
<p class="fr-badge fr-badge--pink-tuile"><%= t('.editor_tokens_table.inactive') %></p>
<% end %>
</td>
<td><%= friendly_date_from_timestamp(editor_token.exp) %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
</div>
<% end %>

<div class="fr-table fr-table--bordered fr-table--layout-fixed">
<div class="fr-table__wrapper">
<div class="fr-table__container">
Expand Down
7 changes: 7 additions & 0 deletions config/locales/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,13 @@ fr:
status: Statut
status_active: Actif
status_revoked: Révoqué
editor_tokens_title: Jetons de délégation
editor_tokens_table:
token: Jeton
status: Statut
expiration: Expiration
copy: Copier le jeton
inactive: Inactif
provider:
dashboard:
show:
Expand Down
4 changes: 3 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
resource :ban, only: %i[new create], controller: 'tokens/bans'
end
end
resources :editors, only: %i[index edit update]
resources :editors, only: %i[index edit update] do
resources :editor_tokens, only: %i[create]
end
resources :provider_dashboards, only: %i[index show], path: 'providers'
resources :audit_notifications, only: %i[index new create]
resources :api_requests, only: %i[index create]
Expand Down
13 changes: 13 additions & 0 deletions db/migrate/20260318100000_create_editor_tokens.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class CreateEditorTokens < ActiveRecord::Migration[8.0]
def change
create_table :editor_tokens, id: :uuid do |t|
t.uuid :editor_id, null: false
t.integer :iat
t.integer :exp, null: false
t.datetime :blacklisted_at
t.timestamps
end

add_foreign_key :editor_tokens, :editors, validate: false
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class ValidateEditorTokensForeignKeys < ActiveRecord::Migration[8.0]
def change
validate_foreign_key :editor_tokens, :editors
end
end
16 changes: 13 additions & 3 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions spec/factories/editor_tokens.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FactoryBot.define do
factory :editor_token do
editor
iat { Time.zone.now.to_i }
exp { 18.months.from_now.to_i }

trait :expired do
exp { 1.month.ago.to_i }
end

trait :blacklisted do
blacklisted_at { 1.month.ago }
end
end
end
1 change: 1 addition & 0 deletions spec/models/editor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

describe 'associations' do
it { is_expected.to have_many(:editor_delegations).dependent(:destroy) }
it { is_expected.to have_many(:editor_tokens).dependent(:destroy) }
end

describe '.delegable' do
Expand Down
Loading