diff --git a/app/controllers/admin/editors_controller.rb b/app/controllers/admin/editors_controller.rb new file mode 100644 index 000000000..281d5a691 --- /dev/null +++ b/app/controllers/admin/editors_controller.rb @@ -0,0 +1,5 @@ +class Admin::EditorsController < AdminController + def index + @editors = Editor.includes(:users).page(params[:page]) + end +end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index d54711dce..0ce3d2be3 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -1,9 +1,26 @@ class Admin::UsersController < AdminController def index - @q = User.ransack(params[:q]) + @q = User.includes(:editor).ransack(params[:q]) @users = @q.result(distinct: true).page(params[:page]) end + def edit + @user = User.find(params[:id]) + @editors = Editor.all + end + + def update + @user = User.find(params[:id]) + + if @user.update(user_params) + success_message(title: "Utilisateur #{@user.email} a bien été modifié") + + redirect_to admin_users_path + else + render :edit + end + end + def impersonate user = User.find(params[:id]) @@ -17,4 +34,10 @@ def stop_impersonating redirect_to admin_users_path end + + private + + def user_params + params.require(:user).permit(:editor_id) + end end diff --git a/app/controllers/editor/authorization_requests_controller.rb b/app/controllers/editor/authorization_requests_controller.rb new file mode 100644 index 000000000..ac618c2cb --- /dev/null +++ b/app/controllers/editor/authorization_requests_controller.rb @@ -0,0 +1,10 @@ +class Editor::AuthorizationRequestsController < EditorController + def index + @authorization_requests = current_editor + .authorization_requests(api: namespace) + .includes(:active_token) + .where( + status: 'validated' + ).page(params[:page]) + end +end diff --git a/app/controllers/editor_controller.rb b/app/controllers/editor_controller.rb new file mode 100644 index 000000000..a9ef94221 --- /dev/null +++ b/app/controllers/editor_controller.rb @@ -0,0 +1,24 @@ +class EditorController < ApplicationController + include AuthenticatedUserManagement + + before_action :user_is_editor? + helper_method :current_editor + + layout 'editor' + + protected + + def current_editor + @current_editor ||= current_user.editor + end + + private + + def user_is_editor? + redirect_to_root unless current_user.editor? + end + + def namespace + request.host.split('.').first + end +end diff --git a/app/helpers/external_url_helper.rb b/app/helpers/external_url_helper.rb index 8d1ea0d49..b40085aaf 100644 --- a/app/helpers/external_url_helper.rb +++ b/app/helpers/external_url_helper.rb @@ -22,6 +22,21 @@ def datapass_base_url end end + def datapass_v2_public_authorization_request_url(authorization_request) + "#{datapass_v2_base_url(authorization_request.api)}/public/demandes/#{authorization_request.public_id}" + end + + def datapass_v2_base_url(api) + case Rails.env + when 'staging' + "https://staging.api-#{api}.v2.datapass.api.gouv.fr" + when 'sandbox' + "https://sandbox.api-#{api}.v2.datapass.api.gouv.fr" + else + "https://api-#{api}.v2.datapass.api.gouv.fr" + end + end + private def highlight_section(prolong_token_wizard) diff --git a/app/lib/seeds.rb b/app/lib/seeds.rb index b8bf8205d..6ee5fbfd8 100644 --- a/app/lib/seeds.rb +++ b/app/lib/seeds.rb @@ -6,6 +6,7 @@ def perform @contact_email = 'contact_technique@yopmail.com' @contact = create_contact + create_editor create_data_for_api_entreprise create_data_for_api_particulier create_data_shared @@ -65,6 +66,19 @@ def create_contact ) end + def create_editor + editor = Editor.create!( + name: 'UMAD Corp', + form_uids: %w[umadcorp-form-api-entreprise umadcorp-form-api-particulier] + ) + create_user( + email: 'editeur@yopmail.com', + first_name: 'Edouard', + last_name: 'Lefevre', + editor: editor + ) + end + def create_magic_link MagicLink.create!(email: @user.email) end @@ -81,6 +95,7 @@ def create_api_entreprise_token_valid external_id: 102, status: :validated, first_submitted_at: 2.weeks.ago, + demarche: 'umadcorp-form-api-entreprise', siret: '12000101100010' } ) @@ -163,6 +178,7 @@ def create_api_particulier_token_valid intitule: 'Mairie de Bordeaux', external_id: 201, status: :validated, + demarche: 'umadcorp-form-api-particulier', first_submitted_at: 2.weeks.ago } ) diff --git a/app/mailers/api_particulier/reporters_mailer.rb b/app/mailers/api_particulier/reporters_mailer.rb index cb47bb9dd..19329b7b9 100644 --- a/app/mailers/api_particulier/reporters_mailer.rb +++ b/app/mailers/api_particulier/reporters_mailer.rb @@ -1,4 +1,6 @@ class APIParticulier::ReportersMailer < APIParticulierMailer + include ExternalUrlHelper + skip_before_action :attach_logos helper_method :datapass_v2_public_authorization_request_url @@ -25,21 +27,6 @@ class APIParticulier::ReportersMailer < APIParticulierMailer private - def datapass_v2_public_authorization_request_url(authorization_request) - "#{datapass_v2_base_url(authorization_request.api)}/public/demandes/#{authorization_request.public_id}" - end - - def datapass_v2_base_url(api) - case Rails.env - when 'staging' - "https://staging.api-#{api}.v2.datapass.api.gouv.fr" - when 'sandbox' - "https://sandbox.api-#{api}.v2.datapass.api.gouv.fr" - else - "https://api-#{api}.v2.datapass.api.gouv.fr" - end - end - def reporter_emails(groups) reporters_config.values_at(*groups).flatten end diff --git a/app/models/editor.rb b/app/models/editor.rb new file mode 100644 index 000000000..14545a1b0 --- /dev/null +++ b/app/models/editor.rb @@ -0,0 +1,12 @@ +class Editor < ApplicationRecord + has_many :users, + dependent: :nullify + + validates :name, presence: true + + def authorization_requests(api:) + AuthorizationRequest + .where(api:) + .where(demarche: form_uids) + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 2483e4239..26da17fc2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -6,6 +6,9 @@ class User < ApplicationRecord has_many :tokens, through: :authorization_requests + belongs_to :editor, + optional: true + validates :email, presence: true, uniqueness: { case_sensitive: false }, @@ -59,6 +62,10 @@ def sanitize_email self.email = email.downcase.strip end + def editor? + editor.present? + end + def admin? if Rails.env.production? Rails.application.credentials.admin_emails.include?(email) diff --git a/app/views/admin/editors/index.html.erb b/app/views/admin/editors/index.html.erb new file mode 100644 index 000000000..599618e5e --- /dev/null +++ b/app/views/admin/editors/index.html.erb @@ -0,0 +1,59 @@ +
+ <%= attr %> + | + <% end %> +|||
---|---|---|---|
+ <%= editor.id %> + | ++ <%= editor.name %> + | +
+
|
+
+ <% if editor.users %>
+
|
+
+ <%= attr %> + | + <% end %> +|||
---|---|---|---|
+ <%= link_to("DataPass ##{authorization_request.external_id}", "#{datapass_v2_base_url(authorization_request.api)}/public/demandes/#{authorization_request.public_id}", target: '_blank')%> + | ++ <%= authorization_request.intitule %> + | ++ <% if authorization_request.token %> + <%= render partial: 'shared/tokens/detail_short', locals: { token: authorization_request.token.decorate } %> + <% end %> + | ++ + <%= authorization_request.siret %> + + | +
+ République
+
Française
+
-
"> - <%= t(".status.label.#{token.status}") %> -
- - | <%= t(".expiration_date", - expiration_date: friendly_date_from_timestamp(token.end_timestamp) - ).html_safe %> - - -"> + <%= t(".status.label.#{token.status}") %> +
+ + | <%= t(".expiration_date", + expiration_date: friendly_date_from_timestamp(token.end_timestamp) + ).html_safe %> + <% end %> - - diff --git a/config/application.rb b/config/application.rb index 182023b30..84d5621d7 100644 --- a/config/application.rb +++ b/config/application.rb @@ -48,5 +48,11 @@ class Application < Rails::Application config.cache_store = :redis_cache_store, config_for(:cache_redis) config.active_support.to_time_preserves_timezone = :zone + + config.generators do |g| + g.test_framework :rspec + g.stylesheets false + g.javascripts false + end end end diff --git a/config/environments/development.rb b/config/environments/development.rb index 7c3e36a05..710ddf560 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -75,7 +75,7 @@ # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. - # config.action_view.annotate_rendered_view_with_filenames = true + config.action_view.annotate_rendered_view_with_filenames = true # Use an evented file watcher to asynchronously detect changes in source code, # routes, locales, etc. This feature depends on the listen gem. diff --git a/config/routes.rb b/config/routes.rb index 6331b10d7..258aff484 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,10 +8,17 @@ get '/admin', to: redirect('/admin/users') namespace :admin do - resources :users, only: %i[index] do + resources :users, only: %i[index edit update] do post :impersonate, on: :member post :stop_impersonating, on: :collection end + resources :editors, only: %i[index] + end + + get '/editeur', to: redirect('/editeur/habilitations'), as: :editor + + namespace :editor, path: 'editeur' do + resources :authorization_requests, only: %i[index], path: 'habilitations' end namespace :api do diff --git a/db/migrate/20241207111810_create_editors.rb b/db/migrate/20241207111810_create_editors.rb new file mode 100644 index 000000000..28827b02e --- /dev/null +++ b/db/migrate/20241207111810_create_editors.rb @@ -0,0 +1,15 @@ +class CreateEditors < ActiveRecord::Migration[8.0] + disable_ddl_transaction! + + def change + create_table :editors, id: :uuid do |t| + t.string :name, null: false + t.string :form_uids, array: true, default: [] + + t.timestamps + end + + add_column :users, :editor_id, :uuid + add_index :users, :editor_id, algorithm: :concurrently + end +end diff --git a/db/schema.rb b/db/schema.rb index 6392e6ae5..76af4cb5a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_09_10_082154) do +ActiveRecord::Schema[8.0].define(version: 2024_12_07_111810) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gin" enable_extension "pg_catalog.plpgsql" @@ -39,6 +39,13 @@ t.index ["external_id"], name: "index_authorization_requests_on_external_id", unique: true, where: "(external_id IS NOT NULL)" end + create_table "editors", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| + t.string "name", null: false + t.string "form_uids", default: [], array: true + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "good_job_batches", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -187,7 +194,9 @@ t.string "first_name" t.string "last_name" t.string "phone_number" + t.uuid "editor_id" t.index ["created_at"], name: "index_users_on_created_at" + t.index ["editor_id"], name: "index_users_on_editor_id" t.index ["email"], name: "index_users_on_email", unique: true end diff --git a/spec/factories/editors.rb b/spec/factories/editors.rb new file mode 100644 index 000000000..332d8a22b --- /dev/null +++ b/spec/factories/editors.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :editor do + name { 'UMAD Editor' } + form_uids do + [ + 'umad-editor' + ] + end + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index d32ac3e2e..4ccbfbab2 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -10,6 +10,10 @@ email { generate(:admin_email) } end + trait :editor do + editor + end + trait :with_full_name do first_name { 'Jean-Marie' } last_name { 'Gigot' } diff --git a/spec/features/admin/editors_spec.rb b/spec/features/admin/editors_spec.rb new file mode 100644 index 000000000..3a07081e6 --- /dev/null +++ b/spec/features/admin/editors_spec.rb @@ -0,0 +1,24 @@ +RSpec.describe 'Admin: editors', app: :api_entreprise do + let(:admin) { create(:user, :admin) } + + before do + login_as(admin) + end + + describe 'index' do + let!(:editor) { create(:editor) } + let!(:editor_user) { create(:user, editor:) } + + before do + visit admin_editors_path + end + + it 'displays editors' do + expect(page).to have_css('.editor', count: 1) + + expect(page).to have_content(editor.name) + expect(page).to have_content(editor_user.email) + expect(page).to have_content(editor.form_uids.first) + end + end +end diff --git a/spec/features/admin/users_spec.rb b/spec/features/admin/users_spec.rb index d79280a11..7b31d4528 100644 --- a/spec/features/admin/users_spec.rb +++ b/spec/features/admin/users_spec.rb @@ -42,4 +42,26 @@ expect(page).to have_no_content(invalid_user.email) end end + + describe 'adding an editor to a specific user' do + subject(:add_editor) do + visit admin_users_path + + click_on dom_id(user, :edit) + + select editor.name, from: 'user_editor_id' + + click_on 'submit' + end + + let!(:user) { create(:user, editor: nil) } + let!(:editor) { create(:editor) } + + it 'adds the editor to the user' do + add_editor + + expect(user.reload.editor).to eq(editor) + expect(page).to have_css('.fr-alert.fr-alert--success') + end + end end diff --git a/spec/features/api_particulier/authorization_request_index_spec.rb b/spec/features/api_particulier/authorization_request_index_spec.rb index d56e1a752..284d937ad 100644 --- a/spec/features/api_particulier/authorization_request_index_spec.rb +++ b/spec/features/api_particulier/authorization_request_index_spec.rb @@ -132,7 +132,9 @@ expect(page).to have_css('#' << dom_id(token)) end - expect(page).to have_no_css('#' << dom_id(token_archived)) + within('#' << dom_id(token_archived)) do + expect(page).to have_text('Aucun jeton actif') + end expect(page).to have_text('☠️ Expiré') expect(page).to have_text('🚫 Banni') diff --git a/spec/features/editor/access_spec.rb b/spec/features/editor/access_spec.rb new file mode 100644 index 000000000..b3f10ef78 --- /dev/null +++ b/spec/features/editor/access_spec.rb @@ -0,0 +1,41 @@ +RSpec.describe 'Editor: access', app: :api_entreprise do + subject(:visit_editor) do + visit editor_path + end + + context 'without user' do + it 'redirects to login path' do + visit_editor + + expect(page).to have_current_path(login_path, ignore_query: true) + end + end + + context 'with user' do + let(:user) { create(:user) } + + before do + login_as(user) + end + + it 'redirects to root' do + visit_editor + + expect(page).to have_current_path(root_path, ignore_query: true) + end + end + + context 'with editor' do + let(:user) { create(:user, :editor) } + + before do + login_as(user) + end + + it 'does not redirect to root' do + visit_editor + + expect(page).to have_current_path(editor_authorization_requests_path, ignore_query: true) + end + end +end diff --git a/spec/features/editor/authorization_requests_spec.rb b/spec/features/editor/authorization_requests_spec.rb new file mode 100644 index 000000000..1e54a65f4 --- /dev/null +++ b/spec/features/editor/authorization_requests_spec.rb @@ -0,0 +1,36 @@ +RSpec.describe 'Editor: authorization requests', app: :api_entreprise do + let(:user) { create(:user, editor:) } + let(:editor) { create(:editor, form_uids: %w[form1 form2]) } + + before do + login_as(user) + end + + describe 'index' do + let!(:valid_authorization_requests) do + [ + create(:authorization_request, :validated, :with_multiple_tokens_one_valid, api: 'entreprise', demarche: 'form1'), + create(:authorization_request, :validated, api: 'entreprise', demarche: 'form2') + ] + end + + let!(:invalid_authorization_requests) do + [ + create(:authorization_request, :validated, api: 'entreprise', demarche: 'wrong_form'), + create(:authorization_request, :validated, api: 'particulier', demarche: 'form1'), + create(:authorization_request, api: 'entreprise', demarche: 'form1') + ] + end + + it 'displays authorization requests linked to editor with token status' do + visit editor_authorization_requests_path + + expect(page).to have_css('.authorization-request', count: 2) + + expect(page).to have_css('#' << dom_id(valid_authorization_requests[0])) + expect(page).to have_css('#' << dom_id(valid_authorization_requests[1])) + + expect(page).to have_content('Nouveau jeton à utiliser') + end + end +end diff --git a/spec/models/editor_spec.rb b/spec/models/editor_spec.rb new file mode 100644 index 000000000..d534ba956 --- /dev/null +++ b/spec/models/editor_spec.rb @@ -0,0 +1,29 @@ +RSpec.describe Editor do + it 'has a valid factory' do + expect(build(:editor)).to be_valid + end + + describe 'authorization_requests association' do + subject { editor.authorization_requests(api:) } + + let(:editor) { create(:editor, form_uids: %w[form1 form2]) } + let(:api) { 'entreprise' } + + let!(:valid_authorization_requests) do + [ + create(:authorization_request, api: 'entreprise', demarche: 'form1'), + create(:authorization_request, api: 'entreprise', demarche: 'form2') + ] + end + + let!(:invalid_authorization_requests) do + [ + create(:authorization_request, api: 'entreprise', demarche: 'wrong_form'), + create(:authorization_request, api: 'particulier', demarche: 'form1') + ] + end + + it { is_expected.to match_array(valid_authorization_requests) } + it { is_expected.to be_a(ActiveRecord::Relation) } + end +end