From d9398e3725f300cd26fcbd40f720c05f13c3fd55 Mon Sep 17 00:00:00 2001
From: vmaubert <github@vcmb.dev>
Date: Wed, 7 Aug 2024 17:47:11 +0200
Subject: [PATCH] refactor(utilisateurs): passe en REST l'appel de la page
 utilisateurs (#1413)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* refactor(utilisateurs): passe en REST l'appel de la page utilisateurs

* fix build ui

* ajout du tri

* linting et plus encore

* amélioration typage

* ajout de tests d'intégration

* export des utilisateurs

* fix lint

* tests

* ajout de tests d'intégration

* fix integration

* first fix ci

* second ci fix

* third ci fix

---------

Co-authored-by: Anis Safine Laget <anis@poensis.fr>
---
 packages/api/package.json                     |  17 +-
 packages/api/src/api/graphql/resolvers.ts     |   3 +-
 .../api/graphql/resolvers/_titre-activite.ts  |  11 +-
 .../api/graphql/resolvers/titres-activites.ts |  18 +-
 .../src/api/graphql/resolvers/utilisateurs.ts |  97 +----------
 .../titres.test.integration.ts.snap           |  46 -----
 .../utilisateurs.test.integration.ts.snap     |  73 ++++++++
 .../api/src/api/rest/activites.queries.ts     |   1 -
 .../src/api/rest/administrations.queries.ts   |   1 -
 .../api/src/api/rest/demarches.queries.ts     |   1 -
 .../entreprises-etablissements.queries.ts     |   1 -
 .../api/src/api/rest/entreprises.queries.ts   |   1 -
 packages/api/src/api/rest/etapes.queries.ts   |   1 -
 .../api/src/api/rest/format/utilisateurs.ts   |   2 +-
 packages/api/src/api/rest/journal.queries.ts  |   1 -
 packages/api/src/api/rest/logs.queries.ts     |   1 -
 .../api/src/api/rest/perimetre.queries.ts     |   1 -
 .../api/rest/statistiques/datagouv.queries.ts |   1 -
 .../src/api/rest/statistiques/dgtm.queries.ts |   1 -
 .../statistiques/evolution-titres.queries.ts  |   1 -
 .../statistiques/metaux-metropole.queries.ts  |   1 -
 .../api/src/api/rest/titre-demande.queries.ts |   1 -
 packages/api/src/api/rest/titres.queries.ts   |   1 -
 .../api/rest/utilisateurs.test.integration.ts |  83 +++++++++
 packages/api/src/api/rest/utilisateurs.ts     |  94 +++++-----
 ...-etapes-heritage-contenu-update.queries.ts |   1 -
 .../processes/titres-phases-update.queries.ts |   1 -
 .../processes/titres-public-update.queries.ts |   1 -
 .../src/database/queries/communes.queries.ts  |   1 -
 .../utilisateurs.test.integration.ts.snap     |  86 ----------
 .../utilisateurs.test.integration.ts          |  51 ------
 .../database/queries/titres-etapes.queries.ts |   1 -
 .../queries/titres-utilisateurs.queries.ts    |   1 -
 .../queries/utilisateurs.queries.test.ts      |  57 ++++++
 .../database/queries/utilisateurs.queries.ts  | 158 +++++++++++++++++
 .../queries/utilisateurs.queries.types.ts     |  41 +++++
 .../queries/utilisateurs.test.integration.ts  |  75 ++++++++
 .../api/src/database/queries/utilisateurs.ts  | 162 +-----------------
 packages/api/src/server/rest.ts               |  15 +-
 packages/api/src/tools/api-mailjet/emails.ts  |   2 +
 packages/api/src/types.ts                     |   4 +-
 packages/api/tests/_utils/index.ts            |   3 +-
 .../common/src/permissions/utilisateurs.ts    |   3 +-
 packages/common/src/rest.ts                   |  10 +-
 packages/common/src/roles.ts                  |   2 +-
 packages/common/src/utilisateur.ts            |  40 ++++-
 packages/ui/src/api/client-rest.ts            |   2 +-
 packages/ui/src/components/_common/liste.tsx  |  13 +-
 .../src/components/administration.stories.tsx |   1 +
 packages/ui/src/components/administration.tsx |  11 +-
 .../utilisateur/utilisateur-api-client.ts     |  60 +------
 .../src/components/utilisateurs.stories.tsx   |   6 +
 packages/ui/src/components/utilisateurs.tsx   |   7 +-
 .../ui/src/components/utilisateurs/table.ts   |  17 +-
 54 files changed, 672 insertions(+), 619 deletions(-)
 create mode 100644 packages/api/src/api/rest/__snapshots__/utilisateurs.test.integration.ts.snap
 delete mode 100644 packages/api/src/database/queries/permissions/__snapshots__/utilisateurs.test.integration.ts.snap
 delete mode 100644 packages/api/src/database/queries/permissions/utilisateurs.test.integration.ts
 create mode 100644 packages/api/src/database/queries/utilisateurs.queries.test.ts
 create mode 100644 packages/api/src/database/queries/utilisateurs.queries.ts
 create mode 100644 packages/api/src/database/queries/utilisateurs.queries.types.ts
 create mode 100644 packages/api/src/database/queries/utilisateurs.test.integration.ts

diff --git a/packages/api/package.json b/packages/api/package.json
index 3104052af..256e2bdbb 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -140,6 +140,19 @@
         "rules": {
           "@typescript-eslint/no-unsafe-declaration-merging": "off"
         }
+      },
+      {
+        "files": ["src/**/*.ts"],
+        "excludedFiles": ["src/**/*.queries.ts"],
+        "rules": {
+          "no-restricted-syntax": [
+            "error",
+            {
+              "message": "dbQueryAndValidate is to be used only in .queries.ts files",
+              "selector": "CallExpression[callee.name='dbQueryAndValidate']"
+            }
+          ]
+        }
       }
     ],
     "extends": [
@@ -191,10 +204,6 @@
         {
           "message": "no 'run' call from PgTyped allowed. Use dbQueryAndValidate.",
           "selector": "CallExpression[callee.property.name='run'][arguments.length=2]"
-        },
-        {
-          "message": "dbQueryAndValidate is to be used only in .queries.ts files",
-          "selector": "CallExpression[callee.name='dbQueryAndValidate']"
         }
       ],
       "no-console": [
diff --git a/packages/api/src/api/graphql/resolvers.ts b/packages/api/src/api/graphql/resolvers.ts
index d0dd7eacb..7f6959bae 100644
--- a/packages/api/src/api/graphql/resolvers.ts
+++ b/packages/api/src/api/graphql/resolvers.ts
@@ -6,7 +6,7 @@ import { etapeHeritage } from './resolvers/titres-etapes'
 
 import { demarches, demarcheCreer, demarcheModifier } from './resolvers/titres-demarches'
 
-import { utilisateur, utilisateurs } from './resolvers/utilisateurs'
+import { utilisateur } from './resolvers/utilisateurs'
 
 import {
   devises,
@@ -65,7 +65,6 @@ export default {
   titres,
   substances,
   utilisateur,
-  utilisateurs,
   statistiquesGlobales,
   activites,
   administrationsTypes,
diff --git a/packages/api/src/api/graphql/resolvers/_titre-activite.ts b/packages/api/src/api/graphql/resolvers/_titre-activite.ts
index 509d7dfa4..c631b72ce 100644
--- a/packages/api/src/api/graphql/resolvers/_titre-activite.ts
+++ b/packages/api/src/api/graphql/resolvers/_titre-activite.ts
@@ -1,4 +1,4 @@
-import { IContenu, ITitreActivite, IUtilisateur } from '../../../types'
+import { IContenu, ITitreActivite } from '../../../types'
 
 import { emailsWithTemplateSend } from '../../../tools/api-mailjet/emails'
 import { activiteUrlGet } from '../../../business/utils/urls-get'
@@ -18,10 +18,6 @@ const titreActiviteEmailTitleFormat = (activite: ITitreActivite, titreNom: strin
   return `${titreNom} | ${activiteType.nom}, ${getPeriode(activiteType.frequenceId, activite.periodeId)} ${activite.annee}`
 }
 
-const titreActiviteUtilisateursEmailsGet = (utilisateurs: IUtilisateur[] | undefined | null): string[] => {
-  return utilisateurs?.map(u => u.email).filter(isNotNullNorUndefinedNorEmpty) ?? []
-}
-
 export const productionCheck = (activiteTypeId: string, contenu: IContenu | null | undefined): boolean => {
   if (activiteTypeId === 'grx' || activiteTypeId === 'gra') {
     if (contenu?.substancesFiscales) {
@@ -85,12 +81,11 @@ export const titreActiviteEmailsSend = async (
   activite: ITitreActivite,
   titreNom: string,
   user: UserNotNull,
-  utilisateurs: IUtilisateur[] | undefined | null,
+  utilisateurEmails: string[],
   administrationIds: NonEmptyArray<AdministrationId>,
   pool: Pool
 ): Promise<void> => {
-  const emails = titreActiviteUtilisateursEmailsGet(utilisateurs)
-
+  const emails = [...utilisateurEmails]
   const administrationsActivitesTypesEmails = await getActiviteTypeEmailsByAdministrationIds(pool, administrationIds)
   emails.push(...titreActiviteAdministrationsEmailsGet(administrationIds, administrationsActivitesTypesEmails, activite.typeId, activite.contenu))
   if (!emails.length) {
diff --git a/packages/api/src/api/graphql/resolvers/titres-activites.ts b/packages/api/src/api/graphql/resolvers/titres-activites.ts
index 4ede90315..9383dddbe 100644
--- a/packages/api/src/api/graphql/resolvers/titres-activites.ts
+++ b/packages/api/src/api/graphql/resolvers/titres-activites.ts
@@ -1,6 +1,6 @@
 import { GraphQLResolveInfo } from 'graphql'
 
-import { Context, ITitre, ITitreActiviteColonneId, IUtilisateur } from '../../../types'
+import { Context, ITitre, ITitreActiviteColonneId } from '../../../types'
 import { ACTIVITES_STATUTS_IDS } from 'camino-common/src/static/activitesStatuts'
 
 import { titreActiviteEmailsSend } from './_titre-activite'
@@ -8,7 +8,6 @@ import { titreActiviteEmailsSend } from './_titre-activite'
 import { fieldsBuild } from './_fields-build'
 
 import { titreActiviteGet, titreActiviteUpdate as titreActiviteUpdateQuery, titresActivitesCount, titresActivitesGet } from '../../../database/queries/titres-activites'
-import { utilisateursGet } from '../../../database/queries/utilisateurs'
 
 import { userSuper } from '../../../database/user-super'
 import { titreGet } from '../../../database/queries/titres'
@@ -28,6 +27,7 @@ import {
 import { ActiviteId } from 'camino-common/src/activite'
 import { getSectionsWithValue } from 'camino-common/src/static/titresTypes_demarchesTypes_etapesTypes/sections'
 import TitresActivites from '../../../database/models/titres-activites'
+import { getUtilisateursEmailsByEntrepriseIds } from '../../../database/queries/utilisateurs.queries'
 
 /**
  * Retourne les activités
@@ -207,15 +207,9 @@ export const activiteDeposer = async ({ id }: { id: ActiviteId }, { user, pool }
 
     const entreprisesIds = isAmodiataire ? titre.amodiataireIds : titre.titulaireIds
 
-    let utilisateurs: IUtilisateur[] = []
-    if (entreprisesIds?.length) {
-      utilisateurs = await utilisateursGet(
-        {
-          entreprisesIds,
-        },
-        { fields: {} },
-        userSuper
-      )
+    let utilisateursEmails: string[] = []
+    if (isNonEmptyArray(entreprisesIds)) {
+      utilisateursEmails = await getUtilisateursEmailsByEntrepriseIds(pool, entreprisesIds)
     }
 
     const administrations: AdministrationId[] = getGestionnairesByTitreTypeId(titre.typeId).map(({ administrationId }) => administrationId)
@@ -226,7 +220,7 @@ export const activiteDeposer = async ({ id }: { id: ActiviteId }, { user, pool }
 
     const filteredAdministrationId = administrations.filter(onlyUnique)
     if (isNonEmptyArray(filteredAdministrationId)) {
-      await titreActiviteEmailsSend(activiteRes, activiteRes.titre!.nom, user, utilisateurs, filteredAdministrationId, pool)
+      await titreActiviteEmailsSend(activiteRes, activiteRes.titre!.nom, user, utilisateursEmails, filteredAdministrationId, pool)
     }
 
     return activiteRes
diff --git a/packages/api/src/api/graphql/resolvers/utilisateurs.ts b/packages/api/src/api/graphql/resolvers/utilisateurs.ts
index 75e3cf4ad..99069117a 100644
--- a/packages/api/src/api/graphql/resolvers/utilisateurs.ts
+++ b/packages/api/src/api/graphql/resolvers/utilisateurs.ts
@@ -1,13 +1,13 @@
 import { GraphQLResolveInfo } from 'graphql'
 
-import { Context, IUtilisateursColonneId } from '../../../types'
+import { Context } from '../../../types'
 
 import { fieldsBuild } from './_fields-build'
 
-import { userGet, utilisateurGet, utilisateursCount, utilisateursGet } from '../../../database/queries/utilisateurs'
+import { userGet, utilisateurGet } from '../../../database/queries/utilisateurs'
 
-import { Role, UtilisateurId } from 'camino-common/src/roles'
-import { canReadUtilisateurs, canReadUtilisateur } from 'camino-common/src/permissions/utilisateurs'
+import { UtilisateurId } from 'camino-common/src/roles'
+import { canReadUtilisateur } from 'camino-common/src/permissions/utilisateurs'
 import { newUtilisateurId } from '../../../database/models/_format/id-create'
 import Utilisateurs from '../../../database/models/utilisateurs'
 
@@ -35,92 +35,3 @@ export const utilisateur = async ({ id }: { id: UtilisateurId }, { user }: Conte
     throw e
   }
 }
-
-export const utilisateurs = async (
-  {
-    intervalle,
-    page,
-    colonne,
-    ordre,
-    entreprisesIds,
-    administrationIds,
-    roles,
-    noms,
-    emails,
-  }: {
-    intervalle?: number | null
-    page?: number | null
-    colonne?: IUtilisateursColonneId | null
-    ordre?: 'asc' | 'desc' | null
-    entreprisesIds?: string[]
-    administrationIds?: string[]
-    roles?: Role[]
-    noms?: string | null
-    emails?: string | null
-  },
-  { user }: Context,
-  info: GraphQLResolveInfo
-): Promise<{
-  elements: Utilisateurs[]
-  page: number | null | undefined
-  intervalle: number | null | undefined
-  ordre: 'asc' | 'desc' | null | undefined
-  colonne: IUtilisateursColonneId | null | undefined
-  total: number
-}> => {
-  try {
-    if (!canReadUtilisateurs(user)) {
-      return {
-        elements: [],
-        page,
-        intervalle,
-        ordre,
-        colonne,
-        total: 0,
-      }
-    }
-    const fields = fieldsBuild(info)
-
-    const [utilisateurs, total] = await Promise.all([
-      utilisateursGet(
-        {
-          intervalle,
-          page,
-          colonne,
-          ordre,
-          entreprisesIds,
-          administrationIds,
-          roles,
-          noms,
-          emails,
-        },
-        { fields: fields.elements },
-        user
-      ),
-      utilisateursCount(
-        {
-          entreprisesIds,
-          administrationIds,
-          roles,
-          noms,
-          emails,
-        },
-        { fields: {} },
-        user
-      ),
-    ])
-
-    return {
-      elements: utilisateurs,
-      page,
-      intervalle,
-      ordre,
-      colonne,
-      total,
-    }
-  } catch (e) {
-    console.error(e)
-
-    throw e
-  }
-}
diff --git a/packages/api/src/api/rest/__snapshots__/titres.test.integration.ts.snap b/packages/api/src/api/rest/__snapshots__/titres.test.integration.ts.snap
index 2583c00b4..9cec6157f 100644
--- a/packages/api/src/api/rest/__snapshots__/titres.test.integration.ts.snap
+++ b/packages/api/src/api/rest/__snapshots__/titres.test.integration.ts.snap
@@ -57,49 +57,3 @@ exports[`titresAdministration > teste la récupération des données pour les Ad
   "type_id": "arm",
 }
 `;
-
-exports[`titresONF > teste la récupération des données pour l'ONF 1`] = `
-{
-  "dateCARM": "",
-  "dateCompletudePTMG": "2022-03-10",
-  "dateReceptionONF": "",
-  "enAttenteDeONF": false,
-  "id": Any<String>,
-  "nom": "titre1",
-  "references": [
-    {
-      "nom": "ONF",
-      "referenceTypeId": "onf",
-    },
-  ],
-  "slug": Any<String>,
-  "titre_statut_id": "mod",
-  "titulaireIds": [
-    "plop",
-  ],
-  "type_id": "arm",
-}
-`;
-
-exports[`titresONF > teste la récupération des données pour l'ONF 2`] = `
-{
-  "dateCARM": "",
-  "dateCompletudePTMG": "",
-  "dateReceptionONF": "",
-  "enAttenteDeONF": false,
-  "id": Any<String>,
-  "nom": "titre2",
-  "references": [
-    {
-      "nom": "ONF",
-      "referenceTypeId": "onf",
-    },
-  ],
-  "slug": Any<String>,
-  "titre_statut_id": "mod",
-  "titulaireIds": [
-    "plop",
-  ],
-  "type_id": "arm",
-}
-`;
diff --git a/packages/api/src/api/rest/__snapshots__/utilisateurs.test.integration.ts.snap b/packages/api/src/api/rest/__snapshots__/utilisateurs.test.integration.ts.snap
new file mode 100644
index 000000000..f5e8b40d4
--- /dev/null
+++ b/packages/api/src/api/rest/__snapshots__/utilisateurs.test.integration.ts.snap
@@ -0,0 +1,73 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`getUtilisateurs > vérifie les droits de lecture > en tant que 'admin' 1`] = `
+{
+  "administrationId": "aut-97300-01",
+  "email": "utilisateurEmail",
+  "id": "utilisateurId",
+  "nom": "utilisateurNom",
+  "prenom": "prenom-pas-super",
+  "role": "editeur",
+  "telephoneFixe": null,
+  "telephoneMobile": null,
+}
+`;
+
+exports[`getUtilisateurs > vérifie les droits de lecture > en tant que 'defaut' 1`] = `
+{
+  "message": "droits insuffisants",
+  "status": 403,
+}
+`;
+
+exports[`getUtilisateurs > vérifie les droits de lecture > en tant que 'editeur' 1`] = `
+{
+  "administrationId": "aut-97300-01",
+  "email": "utilisateurEmail",
+  "id": "utilisateurId",
+  "nom": "utilisateurNom",
+  "prenom": "prenom-pas-super",
+  "role": "editeur",
+  "telephoneFixe": null,
+  "telephoneMobile": null,
+}
+`;
+
+exports[`getUtilisateurs > vérifie les droits de lecture > en tant que 'entreprise' 1`] = `
+{
+  "administrationId": "aut-97300-01",
+  "email": "utilisateurEmail",
+  "id": "utilisateurId",
+  "nom": "utilisateurNom",
+  "prenom": "prenom-pas-super",
+  "role": "editeur",
+  "telephoneFixe": null,
+  "telephoneMobile": null,
+}
+`;
+
+exports[`getUtilisateurs > vérifie les droits de lecture > en tant que 'lecteur' 1`] = `
+{
+  "administrationId": "aut-97300-01",
+  "email": "utilisateurEmail",
+  "id": "utilisateurId",
+  "nom": "utilisateurNom",
+  "prenom": "prenom-pas-super",
+  "role": "editeur",
+  "telephoneFixe": null,
+  "telephoneMobile": null,
+}
+`;
+
+exports[`getUtilisateurs > vérifie les droits de lecture > en tant que 'super' 1`] = `
+{
+  "administrationId": "aut-97300-01",
+  "email": "utilisateurEmail",
+  "id": "utilisateurId",
+  "nom": "utilisateurNom",
+  "prenom": "prenom-pas-super",
+  "role": "editeur",
+  "telephoneFixe": null,
+  "telephoneMobile": null,
+}
+`;
diff --git a/packages/api/src/api/rest/activites.queries.ts b/packages/api/src/api/rest/activites.queries.ts
index 6219c0cbf..b63078876 100644
--- a/packages/api/src/api/rest/activites.queries.ts
+++ b/packages/api/src/api/rest/activites.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { Redefine, dbQueryAndValidate } from '../../pg-database'
 import {
diff --git a/packages/api/src/api/rest/administrations.queries.ts b/packages/api/src/api/rest/administrations.queries.ts
index 456bc37d4..ccc460416 100644
--- a/packages/api/src/api/rest/administrations.queries.ts
+++ b/packages/api/src/api/rest/administrations.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { Pool } from 'pg'
 import { DbQueryAccessError, Redefine, dbQueryAndValidate, effectDbQueryAndValidate } from '../../pg-database'
 import { sql } from '@pgtyped/runtime'
diff --git a/packages/api/src/api/rest/demarches.queries.ts b/packages/api/src/api/rest/demarches.queries.ts
index 77464ddf4..9069204f2 100644
--- a/packages/api/src/api/rest/demarches.queries.ts
+++ b/packages/api/src/api/rest/demarches.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { DemarcheId, DemarcheIdOrSlug } from 'camino-common/src/demarche'
 import { Redefine, dbQueryAndValidate } from '../../pg-database'
diff --git a/packages/api/src/api/rest/entreprises-etablissements.queries.ts b/packages/api/src/api/rest/entreprises-etablissements.queries.ts
index 8683f57b6..eac8aec1b 100644
--- a/packages/api/src/api/rest/entreprises-etablissements.queries.ts
+++ b/packages/api/src/api/rest/entreprises-etablissements.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { Redefine, dbQueryAndValidate } from '../../pg-database'
 import { IGetEntrepriseEtablissementsDbQuery } from './entreprises-etablissements.queries.types'
diff --git a/packages/api/src/api/rest/entreprises.queries.ts b/packages/api/src/api/rest/entreprises.queries.ts
index b7a7162ed..2570d95c2 100644
--- a/packages/api/src/api/rest/entreprises.queries.ts
+++ b/packages/api/src/api/rest/entreprises.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { Redefine, dbQueryAndValidate } from '../../pg-database'
 import {
diff --git a/packages/api/src/api/rest/etapes.queries.ts b/packages/api/src/api/rest/etapes.queries.ts
index 0bdc538c7..cea7c9cc4 100644
--- a/packages/api/src/api/rest/etapes.queries.ts
+++ b/packages/api/src/api/rest/etapes.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { EtapeDocumentId, EtapeId, EtapeIdOrSlug, etapeBrouillonValidator, etapeIdValidator, etapeSlugValidator } from 'camino-common/src/etape'
 import { EtapeTypeId, etapeTypeIdValidator } from 'camino-common/src/static/etapesTypes'
 import { Pool } from 'pg'
diff --git a/packages/api/src/api/rest/format/utilisateurs.ts b/packages/api/src/api/rest/format/utilisateurs.ts
index 987225167..0837e22d4 100644
--- a/packages/api/src/api/rest/format/utilisateurs.ts
+++ b/packages/api/src/api/rest/format/utilisateurs.ts
@@ -2,7 +2,7 @@ import { formatUser, IUtilisateur } from '../../../types'
 import { isAdministration } from 'camino-common/src/roles'
 import { Administrations } from 'camino-common/src/static/administrations'
 
-export const utilisateursFormatTable = (utilisateurs: IUtilisateur[]) =>
+export const utilisateursFormatTable = (utilisateurs: Pick<IUtilisateur, 'email' | 'id' | 'nom' | 'prenom' | 'administrationId' | 'role' | 'entreprises'>[]) =>
   utilisateurs.map(utilisateur => {
     const user = formatUser(utilisateur)
     const lien = isAdministration(user) ? [Administrations[user.administrationId].nom] : utilisateur.entreprises?.map(a => a.nom) ?? []
diff --git a/packages/api/src/api/rest/journal.queries.ts b/packages/api/src/api/rest/journal.queries.ts
index 3d79922bc..90f550a14 100644
--- a/packages/api/src/api/rest/journal.queries.ts
+++ b/packages/api/src/api/rest/journal.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { TitreId } from 'camino-common/src/validators/titres'
 import { Redefine, dbQueryAndValidate } from '../../pg-database'
diff --git a/packages/api/src/api/rest/logs.queries.ts b/packages/api/src/api/rest/logs.queries.ts
index 70502b1a5..21a2f1afb 100644
--- a/packages/api/src/api/rest/logs.queries.ts
+++ b/packages/api/src/api/rest/logs.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { DbQueryAccessError, Redefine, effectDbQueryAndValidate } from '../../pg-database'
 import { Pool } from 'pg'
diff --git a/packages/api/src/api/rest/perimetre.queries.ts b/packages/api/src/api/rest/perimetre.queries.ts
index 9ad95ddb3..94fc17a06 100644
--- a/packages/api/src/api/rest/perimetre.queries.ts
+++ b/packages/api/src/api/rest/perimetre.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { Redefine, DbQueryAccessError, effectDbQueryAndValidate } from '../../pg-database'
 import { z } from 'zod'
diff --git a/packages/api/src/api/rest/statistiques/datagouv.queries.ts b/packages/api/src/api/rest/statistiques/datagouv.queries.ts
index 515be8937..0963d578b 100644
--- a/packages/api/src/api/rest/statistiques/datagouv.queries.ts
+++ b/packages/api/src/api/rest/statistiques/datagouv.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { Redefine, dbQueryAndValidate } from '../../../pg-database'
 import { Pool } from 'pg'
diff --git a/packages/api/src/api/rest/statistiques/dgtm.queries.ts b/packages/api/src/api/rest/statistiques/dgtm.queries.ts
index ca8f3b005..e93533149 100644
--- a/packages/api/src/api/rest/statistiques/dgtm.queries.ts
+++ b/packages/api/src/api/rest/statistiques/dgtm.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { SUBSTANCES_FISCALES_IDS, SubstanceFiscaleId } from 'camino-common/src/static/substancesFiscales'
 import { Redefine, dbQueryAndValidate } from '../../../pg-database'
diff --git a/packages/api/src/api/rest/statistiques/evolution-titres.queries.ts b/packages/api/src/api/rest/statistiques/evolution-titres.queries.ts
index c7c66e6d5..380fcb4bd 100644
--- a/packages/api/src/api/rest/statistiques/evolution-titres.queries.ts
+++ b/packages/api/src/api/rest/statistiques/evolution-titres.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { DemarcheStatutId } from 'camino-common/src/static/demarchesStatuts'
 import { DemarcheTypeId } from 'camino-common/src/static/demarchesTypes'
diff --git a/packages/api/src/api/rest/statistiques/metaux-metropole.queries.ts b/packages/api/src/api/rest/statistiques/metaux-metropole.queries.ts
index 523a96f5f..eef830015 100644
--- a/packages/api/src/api/rest/statistiques/metaux-metropole.queries.ts
+++ b/packages/api/src/api/rest/statistiques/metaux-metropole.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { AnneeCountStatistique, anneeCountStatistiqueValidator } from 'camino-common/src/statistiques'
 import { Redefine, dbQueryAndValidate } from '../../../pg-database'
diff --git a/packages/api/src/api/rest/titre-demande.queries.ts b/packages/api/src/api/rest/titre-demande.queries.ts
index 42b7f2eb2..6e7cf2147 100644
--- a/packages/api/src/api/rest/titre-demande.queries.ts
+++ b/packages/api/src/api/rest/titre-demande.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { DbQueryAccessError, Redefine, effectDbQueryAndValidate } from '../../pg-database.js'
 import { z } from 'zod'
diff --git a/packages/api/src/api/rest/titres.queries.ts b/packages/api/src/api/rest/titres.queries.ts
index 55d070bb7..fbf075173 100644
--- a/packages/api/src/api/rest/titres.queries.ts
+++ b/packages/api/src/api/rest/titres.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { getMostRecentValuePropFromEtapeFondamentaleValide, TitreGet, TitreGetDemarche, titreGetValidator } from 'camino-common/src/titres'
 import { Redefine, dbQueryAndValidate } from '../../pg-database'
diff --git a/packages/api/src/api/rest/utilisateurs.test.integration.ts b/packages/api/src/api/rest/utilisateurs.test.integration.ts
index 25803ac80..86ac367e0 100644
--- a/packages/api/src/api/rest/utilisateurs.test.integration.ts
+++ b/packages/api/src/api/rest/utilisateurs.test.integration.ts
@@ -10,6 +10,8 @@ import { newUtilisateurId } from '../../database/models/_format/id-create'
 import { KeycloakFakeServer, idUserKeycloakRecognised, setupKeycloak, teardownKeycloak } from '../../../tests/keycloak'
 import { renewConfig } from '../../config/index'
 import { utilisateurIdValidator } from 'camino-common/src/roles'
+import { testBlankUser, TestUser } from 'camino-common/src/tests-utils'
+import { Administrations } from 'camino-common/src/static/administrations'
 
 console.info = vi.fn()
 console.error = vi.fn()
@@ -104,6 +106,8 @@ describe('utilisateurSupprimer', () => {
     const user = await userGenerate({ role: 'defaut' })
 
     const tested = await restCall(dbPool, '/rest/utilisateurs/:id/delete', { id: user.id }, { role: 'defaut' })
+    await knex.raw('delete from logs')
+    await knex.raw('delete from utilisateurs') // ce delete est nécessaire car l'utilisateur n'est pas supprimé de la bdd et pour éviter qu'un test plus bas ne tente une nouvelle réinsertion du user `defaut-user`et échoue
     expect(tested.statusCode).toBe(302)
     expect(tested.header.location).toBe(`${OAUTH_URL}/oauth2/sign_out`)
   })
@@ -168,3 +172,82 @@ describe('registerToNewsletter', () => {
     expect(tested.statusCode).toBe(400)
   })
 })
+
+describe('getUtilisateurs', () => {
+  test('retourne la liste ordonnée des utilisateurs', async () => {
+    const id = utilisateurIdValidator.parse('id')
+    await knex('utilisateurs').insert({
+      id,
+      prenom: 'prenom-pas-super',
+      nom: 'nom-pas-super',
+      email: 'prenom-pas-super@camino.local',
+      role: 'defaut',
+      telephone_fixe: '0102030405',
+      dateCreation: '2022-05-12',
+      keycloakId: idUserKeycloakRecognised,
+    })
+
+    const tested = await restNewCall(dbPool, '/rest/utilisateurs', {}, userSuper, { colonne: 'nom', ordre: 'desc', page: 1, intervalle: 10 })
+    expect(tested.statusCode).toBe(200)
+    expect(tested.body).toStrictEqual({
+      elements: [
+        {
+          email: 'super@camino.local',
+          id: 'super',
+          nom: 'nom-super',
+          prenom: 'prenom-super',
+          role: 'super',
+          telephoneFixe: null,
+          telephoneMobile: null,
+        },
+        {
+          email: 'prenom-pas-super@camino.local',
+          id: 'id',
+          nom: 'nom-pas-super',
+          prenom: 'prenom-pas-super',
+          role: 'defaut',
+          telephoneFixe: '0102030405',
+          telephoneMobile: null,
+        },
+      ],
+      total: 2,
+    })
+  })
+
+  describe('vérifie les droits de lecture', async () => {
+    const mockAdministration = Administrations['aut-97300-01']
+    const mockUserNom = 'utilisateurNom'
+    beforeAll(async () => {
+      await knex('utilisateurs').insert({
+        id: newUtilisateurId('utilisateurId'),
+        prenom: 'prenom-pas-super',
+        nom: mockUserNom,
+        email: 'utilisateurEmail',
+        role: 'editeur',
+        administrationId: mockAdministration.id,
+        dateCreation: '2022-05-12',
+        keycloakId: idUserKeycloakRecognised,
+      })
+    })
+
+    test.each<[TestUser, boolean]>([
+      [{ role: 'super' }, true],
+      [{ role: 'admin', administrationId: mockAdministration.id }, true],
+      [{ role: 'editeur', administrationId: mockAdministration.id }, true],
+      [{ role: 'lecteur', administrationId: mockAdministration.id }, true],
+      [{ role: 'entreprise', entreprises: [] }, true],
+      [{ role: 'defaut' }, false],
+    ])('en tant que $role', async (user, voit) => {
+      const result = await restNewCall(dbPool, '/rest/utilisateurs', {}, { ...user, ...testBlankUser }, { colonne: 'nom', ordre: 'asc', page: 1, intervalle: 10, nomsUtilisateurs: mockUserNom })
+
+      if (voit) {
+        expect(result.status).toBe(200)
+        expect(result.body.elements).toHaveLength(1)
+        expect(result.body.elements[0]).toMatchSnapshot()
+      } else {
+        expect(result.status).toBe(403)
+        expect(result.body).toMatchSnapshot()
+      }
+    })
+  })
+})
diff --git a/packages/api/src/api/rest/utilisateurs.ts b/packages/api/src/api/rest/utilisateurs.ts
index 786aec68e..530df3c04 100644
--- a/packages/api/src/api/rest/utilisateurs.ts
+++ b/packages/api/src/api/rest/utilisateurs.ts
@@ -1,24 +1,27 @@
-import { userGet, utilisateurGet, utilisateursGet, utilisateurUpsert } from '../../database/queries/utilisateurs'
+import { userGet, utilisateurGet, utilisateurUpsert } from '../../database/queries/utilisateurs'
 import { CaminoRequest, CustomResponse } from './express-type'
-import { CaminoApiError, formatUser, IUtilisateursColonneId } from '../../types'
+import { CaminoApiError, formatUser } from '../../types'
 import { HTTP_STATUS } from 'camino-common/src/http'
 import { isSubscribedToNewsLetter, newsletterSubscriberUpdate } from '../../tools/api-mailjet/newsletter'
-import { isAdministrationRole, isEntrepriseOrBureauDetudeRole, isRole, User } from 'camino-common/src/roles'
+import { isAdministrationRole, isEntrepriseOrBureauDetudeRole, User } from 'camino-common/src/roles'
 import { utilisateursFormatTable } from './format/utilisateurs'
 import { tableConvert } from './_convert'
 import { fileNameCreate } from '../../tools/file-name-create'
-import { newsletterAbonnementValidator, QGISToken, utilisateurToEdit } from 'camino-common/src/utilisateur'
+import { newsletterAbonnementValidator, QGISToken, utilisateursSearchParamsValidator, UtilisateursTable, utilisateurToEdit } from 'camino-common/src/utilisateur'
 import { knex } from '../../knex'
 import { idGenerate } from '../../database/models/_format/id-create'
 import bcrypt from 'bcryptjs'
 import { utilisateurUpdationValidate } from '../../business/validations/utilisateur-updation-validate'
 import { canDeleteUtilisateur } from 'camino-common/src/permissions/utilisateurs'
-import { DownloadFormat } from 'camino-common/src/rest'
 import { Pool } from 'pg'
 import { DeepReadonly, isNotNullNorUndefined, isNullOrUndefined } from 'camino-common/src/typescript-tools'
 import { config } from '../../config/index'
 import { Effect, Match, pipe } from 'effect'
 import { RestNewGetCall } from '../../server/rest'
+import { getUtilisateursFilteredAndSorted } from '../../database/queries/utilisateurs.queries'
+import { DbQueryAccessError } from '../../pg-database'
+import { callAndExit, ZodUnparseable } from '../../tools/fp-tools'
+import { z } from 'zod'
 
 export const isSubscribedToNewsletter =
   (_pool: Pool) =>
@@ -263,60 +266,49 @@ export const registerToNewsletter: RestNewGetCall<'/rest/utilisateurs/registerTo
   )
 }
 
-interface IUtilisateursQueryInput {
-  format?: DownloadFormat
-  colonne?: IUtilisateursColonneId | null
-  ordre?: 'asc' | 'desc' | null
-  entrepriseIds?: string | string[]
-  administrationIds?: string | string[]
-  //  TODO 2022-06-14: utiliser un tableau de string plutôt qu'une chaine séparée par des ','
-  roles?: string | string[]
-  noms?: string | null
-  nomsUtilisateurs?: string | null
-  emails?: string | null
-}
-
 export const utilisateurs =
-  (_pool: Pool) =>
+  (pool: Pool) =>
   async (
-    { query: { format = 'csv', colonne, ordre, entrepriseIds, administrationIds, roles, noms, emails, nomsUtilisateurs } }: { query: IUtilisateursQueryInput },
+    { query }: { query: unknown },
     user: User
   ): Promise<{
     nom: string
     format: 'csv' | 'xlsx' | 'ods'
     contenu: string
   } | null> => {
-    const utilisateurs = await utilisateursGet(
-      {
-        colonne,
-        ordre,
-        entreprisesIds: isNotNullNorUndefined(entrepriseIds) ? (Array.isArray(entrepriseIds) ? entrepriseIds : entrepriseIds.split(',')) : undefined,
-        administrationIds: isNotNullNorUndefined(administrationIds) ? (Array.isArray(administrationIds) ? administrationIds : administrationIds.split(',')) : undefined,
-        roles: isNotNullNorUndefined(roles) ? (Array.isArray(roles) ? roles.filter(isRole) : roles.split(',').filter(isRole)) : undefined,
-        noms: noms ?? nomsUtilisateurs,
-        emails,
-      },
-      {},
-      user
-    )
+    const searchParams = utilisateursSearchParamsValidator.and(z.object({ format: z.enum(['csv', 'xlsx', 'ods']).optional().default('csv') })).parse(query)
 
-    let contenu
+    return callAndExit(getUtilisateursFilteredAndSorted(pool, user, searchParams), async utilisateurs => {
+      const format = searchParams.format
+      const contenu = tableConvert('utilisateurs', utilisateursFormatTable(utilisateurs), format)
 
-    switch (format) {
-      case 'csv':
-      case 'xlsx':
-      case 'ods':
-        contenu = tableConvert('utilisateurs', utilisateursFormatTable(utilisateurs), format)
-        break
-      default:
-        throw new Error(`Format non supporté ${format}`)
-    }
-
-    return contenu
-      ? {
-          nom: fileNameCreate(`utilisateurs-${utilisateurs.length}`, format),
-          format,
-          contenu,
-        }
-      : null
+      return contenu
+        ? {
+            nom: fileNameCreate(`utilisateurs-${utilisateurs.length}`, format),
+            format,
+            contenu,
+          }
+        : null
+    })
   }
+
+type GetUtilisateursError = DbQueryAccessError | ZodUnparseable | "Impossible d'accéder à la liste des utilisateurs" | 'droits insuffisants'
+export const getUtilisateurs: RestNewGetCall<'/rest/utilisateurs'> = (pool, user, _params, searchParams): Effect.Effect<DeepReadonly<UtilisateursTable>, CaminoApiError<GetUtilisateursError>> => {
+  return Effect.Do.pipe(
+    Effect.flatMap(() => getUtilisateursFilteredAndSorted(pool, user, searchParams)),
+    Effect.map(utilisateurs => {
+      return {
+        elements: utilisateurs.slice((searchParams.page - 1) * searchParams.intervalle, searchParams.page * searchParams.intervalle),
+        total: utilisateurs.length,
+      }
+    }),
+    Effect.mapError(caminoError =>
+      Match.value(caminoError.message).pipe(
+        Match.when("Impossible d'accéder à la base de données", () => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR })),
+        Match.when('droits insuffisants', () => ({ ...caminoError, status: HTTP_STATUS.FORBIDDEN })),
+        Match.when('Problème de validation de données', () => ({ ...caminoError, status: HTTP_STATUS.BAD_REQUEST })),
+        Match.exhaustive
+      )
+    )
+  )
+}
diff --git a/packages/api/src/business/processes/titres-etapes-heritage-contenu-update.queries.ts b/packages/api/src/business/processes/titres-etapes-heritage-contenu-update.queries.ts
index 0675f0c0f..67b512f2a 100644
--- a/packages/api/src/business/processes/titres-etapes-heritage-contenu-update.queries.ts
+++ b/packages/api/src/business/processes/titres-etapes-heritage-contenu-update.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { IGetEtapesByDemarcheInternalQuery } from './titres-etapes-heritage-contenu-update.queries.types'
 import { Redefine, dbQueryAndValidate } from '../../pg-database'
diff --git a/packages/api/src/business/processes/titres-phases-update.queries.ts b/packages/api/src/business/processes/titres-phases-update.queries.ts
index f71bbbf77..35276241b 100644
--- a/packages/api/src/business/processes/titres-phases-update.queries.ts
+++ b/packages/api/src/business/processes/titres-phases-update.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { CaminoDate } from 'camino-common/src/date'
 import { DemarcheId } from 'camino-common/src/demarche'
diff --git a/packages/api/src/business/processes/titres-public-update.queries.ts b/packages/api/src/business/processes/titres-public-update.queries.ts
index ea20c02e1..41e0d98f4 100644
--- a/packages/api/src/business/processes/titres-public-update.queries.ts
+++ b/packages/api/src/business/processes/titres-public-update.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { Redefine, dbQueryAndValidate } from '../../pg-database'
 import { Pool } from 'pg'
diff --git a/packages/api/src/database/queries/communes.queries.ts b/packages/api/src/database/queries/communes.queries.ts
index baf8b70dd..282b0fc4e 100644
--- a/packages/api/src/database/queries/communes.queries.ts
+++ b/packages/api/src/database/queries/communes.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { Redefine, dbQueryAndValidate } from '../../pg-database'
 import { IGetCommuneIdsInternalQuery, IGetCommunesInternalQuery, IInsertCommuneInternalQuery } from './communes.queries.types'
diff --git a/packages/api/src/database/queries/permissions/__snapshots__/utilisateurs.test.integration.ts.snap b/packages/api/src/database/queries/permissions/__snapshots__/utilisateurs.test.integration.ts.snap
deleted file mode 100644
index 4e1a1af02..000000000
--- a/packages/api/src/database/queries/permissions/__snapshots__/utilisateurs.test.integration.ts.snap
+++ /dev/null
@@ -1,86 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`utilisateursQueryModify > Vérifie l'écriture de la requête sur un utilisateur 1`] = `
-{
-  "administrationId": "aut-97300-01",
-  "dateCreation": "2022-05-12",
-  "email": "utilisateurEmail",
-  "entreprises": [],
-  "id": "utilisateurId",
-  "keycloakId": "keycloakId",
-  "nom": "utilisateurNom",
-  "prenom": null,
-  "qgisToken": null,
-  "role": "editeur",
-  "telephoneFixe": null,
-  "telephoneMobile": null,
-}
-`;
-
-exports[`utilisateursQueryModify > Vérifie l'écriture de la requête sur un utilisateur 2`] = `
-{
-  "administrationId": "aut-97300-01",
-  "dateCreation": "2022-05-12",
-  "email": "utilisateurEmail",
-  "entreprises": [],
-  "id": "utilisateurId",
-  "keycloakId": "keycloakId",
-  "nom": "utilisateurNom",
-  "prenom": null,
-  "qgisToken": null,
-  "role": "editeur",
-  "telephoneFixe": null,
-  "telephoneMobile": null,
-}
-`;
-
-exports[`utilisateursQueryModify > Vérifie l'écriture de la requête sur un utilisateur 3`] = `
-{
-  "administrationId": "aut-97300-01",
-  "dateCreation": "2022-05-12",
-  "email": "utilisateurEmail",
-  "entreprises": [],
-  "id": "utilisateurId",
-  "keycloakId": "keycloakId",
-  "nom": "utilisateurNom",
-  "prenom": null,
-  "qgisToken": null,
-  "role": "editeur",
-  "telephoneFixe": null,
-  "telephoneMobile": null,
-}
-`;
-
-exports[`utilisateursQueryModify > Vérifie l'écriture de la requête sur un utilisateur 4`] = `
-{
-  "administrationId": "aut-97300-01",
-  "dateCreation": "2022-05-12",
-  "email": "utilisateurEmail",
-  "entreprises": [],
-  "id": "utilisateurId",
-  "keycloakId": "keycloakId",
-  "nom": "utilisateurNom",
-  "prenom": null,
-  "qgisToken": null,
-  "role": "editeur",
-  "telephoneFixe": null,
-  "telephoneMobile": null,
-}
-`;
-
-exports[`utilisateursQueryModify > Vérifie l'écriture de la requête sur un utilisateur 5`] = `
-{
-  "administrationId": "aut-97300-01",
-  "dateCreation": "2022-05-12",
-  "email": "utilisateurEmail",
-  "entreprises": [],
-  "id": "utilisateurId",
-  "keycloakId": "keycloakId",
-  "nom": "utilisateurNom",
-  "prenom": null,
-  "qgisToken": null,
-  "role": "editeur",
-  "telephoneFixe": null,
-  "telephoneMobile": null,
-}
-`;
diff --git a/packages/api/src/database/queries/permissions/utilisateurs.test.integration.ts b/packages/api/src/database/queries/permissions/utilisateurs.test.integration.ts
deleted file mode 100644
index 5f4b3bca1..000000000
--- a/packages/api/src/database/queries/permissions/utilisateurs.test.integration.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { IUtilisateur } from '../../../types'
-
-import { dbManager } from '../../../../tests/db-manager'
-import Utilisateurs from '../../models/utilisateurs'
-import { utilisateursGet } from '../utilisateurs'
-import { Administrations } from 'camino-common/src/static/administrations'
-import options from '../_options'
-import { beforeAll, expect, afterAll, test, describe, vi } from 'vitest'
-import { testBlankUser, TestUser } from 'camino-common/src/tests-utils'
-import { newUtilisateurId } from '../../models/_format/id-create'
-console.info = vi.fn()
-console.error = vi.fn()
-beforeAll(async () => {
-  await dbManager.populateDb()
-  await Utilisateurs.query().insertGraph(mockUser, options.utilisateurs.update)
-})
-
-afterAll(async () => {
-  await dbManager.closeKnex()
-})
-
-const mockAdministration = Administrations['aut-97300-01']
-
-const mockUser: IUtilisateur = {
-  id: newUtilisateurId('utilisateurId'),
-  role: 'editeur',
-  nom: 'utilisateurNom',
-  email: 'utilisateurEmail',
-  administrationId: mockAdministration.id,
-  dateCreation: '2022-05-12',
-  keycloakId: 'keycloakId',
-}
-
-describe('utilisateursQueryModify', () => {
-  test.each<[TestUser, boolean]>([
-    [{ role: 'super' }, true],
-    [{ role: 'admin', administrationId: mockAdministration.id }, true],
-    [{ role: 'editeur', administrationId: mockAdministration.id }, true],
-    [{ role: 'lecteur', administrationId: mockAdministration.id }, true],
-    [{ role: 'entreprise', entreprises: [] }, true],
-    [{ role: 'defaut' }, false],
-  ])("Vérifie l'écriture de la requête sur un utilisateur", async (user, voit) => {
-    const utilisateurs = await utilisateursGet({ noms: mockUser.nom }, {}, { ...user, ...testBlankUser })
-    if (voit) {
-      expect(utilisateurs).toHaveLength(1)
-      expect(utilisateurs[0]).toMatchSnapshot()
-    } else {
-      expect(utilisateurs).toHaveLength(0)
-    }
-  })
-})
diff --git a/packages/api/src/database/queries/titres-etapes.queries.ts b/packages/api/src/database/queries/titres-etapes.queries.ts
index 1457c3992..dc6746c58 100644
--- a/packages/api/src/database/queries/titres-etapes.queries.ts
+++ b/packages/api/src/database/queries/titres-etapes.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { dbQueryAndValidate, Redefine } from '../../pg-database'
 import {
diff --git a/packages/api/src/database/queries/titres-utilisateurs.queries.ts b/packages/api/src/database/queries/titres-utilisateurs.queries.ts
index 0177eafcd..b3e1c9bea 100644
--- a/packages/api/src/database/queries/titres-utilisateurs.queries.ts
+++ b/packages/api/src/database/queries/titres-utilisateurs.queries.ts
@@ -1,4 +1,3 @@
-/* eslint-disable no-restricted-syntax */
 import { sql } from '@pgtyped/runtime'
 import { Redefine, dbQueryAndValidate } from '../../pg-database'
 import { IGetTitreUtilisateurDbQuery } from './titres-utilisateurs.queries.types'
diff --git a/packages/api/src/database/queries/utilisateurs.queries.test.ts b/packages/api/src/database/queries/utilisateurs.queries.test.ts
new file mode 100644
index 000000000..be2c8b746
--- /dev/null
+++ b/packages/api/src/database/queries/utilisateurs.queries.test.ts
@@ -0,0 +1,57 @@
+import { test, describe, expect } from 'vitest'
+import { type GetUtilisateur, filterUtilisateur } from './utilisateurs.queries'
+import { testBlankUser } from 'camino-common/src/tests-utils'
+import { entrepriseIdValidator } from 'camino-common/src/entreprise'
+import { userSuper } from '../user-super'
+
+const userToBeFiltered: GetUtilisateur = {
+  ...testBlankUser,
+  telephone_fixe: 'telFixe',
+  telephone_mobile: 'telMobile',
+  role: 'defaut',
+  administration_id: null,
+  entreprise_ids: null,
+}
+
+describe('filterUtilisateur', () => {
+  test('filtre les utilisateurs par administration', () => {
+    const adminUser: GetUtilisateur = { ...userToBeFiltered, role: 'admin', administration_id: 'aut-97300-01' }
+    expect(filterUtilisateur(userToBeFiltered, { administrationIds: ['aut-97300-01'] }, userSuper)).toBe(false)
+    expect(filterUtilisateur(adminUser, { administrationIds: ['aut-97300-01'] }, userSuper)).toBe(true)
+  })
+
+  test('filtre les utilisateurs par entreprise', () => {
+    const entrepriseId = entrepriseIdValidator.parse('entrepriseId')
+    const entrepriseUser: GetUtilisateur = { ...userToBeFiltered, role: 'entreprise', entreprise_ids: [entrepriseId] }
+    expect(filterUtilisateur(userToBeFiltered, { entreprisesIds: [entrepriseId] }, userSuper)).toBe(false)
+    expect(filterUtilisateur(entrepriseUser, { entreprisesIds: [entrepriseId] }, userSuper)).toBe(true)
+  })
+
+  test('filtre les utilisateurs par role', () => {
+    expect(filterUtilisateur(userToBeFiltered, { roles: ['admin'] }, userSuper)).toBe(false)
+    expect(filterUtilisateur(userToBeFiltered, { roles: ['defaut'] }, userSuper)).toBe(true)
+  })
+
+  test('filtre les utilisateurs par nom (et ne prend pas en compte la casse)', () => {
+    expect(filterUtilisateur(userToBeFiltered, { nomsUtilisateurs: 'camino' }, userSuper)).toBe(false)
+    expect(filterUtilisateur({ ...userToBeFiltered, nom: 'don Camino' }, { nomsUtilisateurs: 'camino' }, userSuper)).toBe(true)
+    expect(filterUtilisateur({ ...userToBeFiltered, prenom: 'don Camino' }, { nomsUtilisateurs: 'camino' }, userSuper)).toBe(true)
+    expect(filterUtilisateur({ ...userToBeFiltered, prenom: null }, { nomsUtilisateurs: 'camino' }, userSuper)).toBe(false)
+  })
+
+  test('filtre les utilisateurs par email', () => {
+    expect(filterUtilisateur(userToBeFiltered, { emails: 'camino' }, userSuper)).toBe(false)
+    expect(filterUtilisateur({ ...userToBeFiltered, email: 'jean@camino.beta.gouv.fr' }, { emails: 'camino' }, userSuper)).toBe(true)
+  })
+
+  test('filtre les utilisateurs qui ne sont pas de ton administration', () => {
+    expect(filterUtilisateur(userToBeFiltered, {}, { ...testBlankUser, role: 'lecteur', administrationId: 'aut-97300-01' })).toBe(false)
+    expect(filterUtilisateur({ ...userToBeFiltered, role: 'admin', administration_id: 'aut-97300-01' }, {}, { ...testBlankUser, role: 'lecteur', administrationId: 'aut-97300-01' })).toBe(true)
+  })
+
+  test('filtre les utilisateurs qui ne sont pas de ton entreprise', () => {
+    const entrepriseId = entrepriseIdValidator.parse('entrepriseId')
+    expect(filterUtilisateur(userToBeFiltered, {}, { ...testBlankUser, role: 'entreprise', entreprises: [{ id: entrepriseId }] })).toBe(false)
+    expect(filterUtilisateur({ ...userToBeFiltered, role: 'entreprise', entreprise_ids: [entrepriseId] }, {}, { ...testBlankUser, role: 'entreprise', entreprises: [{ id: entrepriseId }] })).toBe(true)
+  })
+})
diff --git a/packages/api/src/database/queries/utilisateurs.queries.ts b/packages/api/src/database/queries/utilisateurs.queries.ts
new file mode 100644
index 000000000..798157ccb
--- /dev/null
+++ b/packages/api/src/database/queries/utilisateurs.queries.ts
@@ -0,0 +1,158 @@
+import { sql } from '@pgtyped/runtime'
+import { Effect, pipe } from 'effect'
+import { DbQueryAccessError, Redefine, dbQueryAndValidate, effectDbQueryAndValidate } from '../../pg-database'
+import {
+  AdminUserNotNull,
+  EntrepriseUserNotNull,
+  User,
+  isAdministrationEditeur,
+  isAdministrationLecteur,
+  isBureauDEtudes,
+  isEntreprise,
+  roleValidator,
+  utilisateurIdValidator,
+} from 'camino-common/src/roles'
+import { z } from 'zod'
+import { CaminoError } from 'camino-common/src/zod-tools'
+import { Pool } from 'pg'
+import { administrationIdValidator } from 'camino-common/src/static/administrations'
+import { canReadUtilisateurs } from 'camino-common/src/permissions/utilisateurs'
+import { UtilisateursSearchParamsInput, UtilisateursSearchParams, Utilisateur, utilisateurValidator } from 'camino-common/src/utilisateur'
+import { IGetUtilisateursDbQuery, IGetUtilisateursEmailsByEntrepriseIdsDbQuery } from './utilisateurs.queries.types'
+import { ZodUnparseable, zodParseEffect } from '../../tools/fp-tools'
+import { DeepReadonly, NonEmptyArray, Nullable, isNotNullNorUndefinedNorEmpty, isNullOrUndefinedOrEmpty } from 'camino-common/src/typescript-tools'
+import { EntrepriseId, entrepriseIdValidator } from 'camino-common/src/entreprise'
+
+const getUtilisateursValidator = z.object({
+  id: utilisateurIdValidator,
+  email: z.string(),
+  nom: z.string(),
+  prenom: z.string().nullable(),
+  telephone_fixe: z.string().nullable(),
+  telephone_mobile: z.string().nullable(),
+  role: roleValidator,
+  administration_id: administrationIdValidator.nullable(),
+  entreprise_ids: z.array(entrepriseIdValidator).nullable(),
+})
+export type GetUtilisateur = z.infer<typeof getUtilisateursValidator>
+
+type GetUtilisateursFilteredAndSortedErrors = DbQueryAccessError | ZodUnparseable | 'droits insuffisants'
+export const getUtilisateursFilteredAndSorted = (
+  pool: Pool,
+  user: DeepReadonly<User>,
+  searchParams: UtilisateursSearchParams
+): Effect.Effect<Utilisateur[], CaminoError<GetUtilisateursFilteredAndSortedErrors>> => {
+  return Effect.Do.pipe(
+    Effect.filterOrFail(
+      () => canReadUtilisateurs(user),
+      () => ({ message: 'droits insuffisants' as const })
+    ),
+    Effect.flatMap(() => effectDbQueryAndValidate(getUtilisateursDb, undefined, pool, getUtilisateursValidator)),
+    Effect.map(utilisateurs => {
+      return utilisateurs.filter(utilisateur => {
+        return filterUtilisateur(utilisateur, searchParams, user)
+      })
+    }),
+    Effect.flatMap(utilisateurs => {
+      return Effect.forEach(utilisateurs, u => {
+        const obj: Pick<Utilisateur, 'telephoneFixe' | 'telephoneMobile' | 'id' | 'nom' | 'prenom' | 'role' | 'email'> &
+          Nullable<Pick<AdminUserNotNull, 'administrationId'>> &
+          Pick<EntrepriseUserNotNull, 'entreprises'> = {
+          ...u,
+          telephoneMobile: u.telephone_mobile,
+          telephoneFixe: u.telephone_fixe,
+          administrationId: u.administration_id,
+          entreprises: u.entreprise_ids?.map(id => ({ id })) ?? [],
+          prenom: u.prenom ?? '',
+        }
+
+        return pipe(
+          zodParseEffect(utilisateurValidator, obj),
+          Effect.mapError(error => {
+            return { ...error, extra: { email: u.email } }
+          })
+        )
+      })
+    }),
+    Effect.map(utilisateurs => {
+      return utilisateurs.toSorted((a, b) => {
+        const result = a[searchParams.colonne].localeCompare(b[searchParams.colonne])
+
+        return result * (searchParams.ordre === 'asc' ? 1 : -1)
+      })
+    })
+  )
+}
+
+// VISIBLE FOR TESTING
+export const filterUtilisateur = (utilisateur: GetUtilisateur, params: Omit<UtilisateursSearchParamsInput, 'page' | 'intervalle' | 'colonne' | 'ordre'>, user: DeepReadonly<User>): boolean => {
+  // On filtre en fonction du user connecté
+  if (isAdministrationEditeur(user) || isAdministrationLecteur(user)) {
+    // un utilisateur 'editeur' ou 'lecteur'
+    // ne voit que les utilisateurs de son administration
+    if (utilisateur.administration_id !== user.administrationId) {
+      return false
+    }
+  } else if ((isEntreprise(user) || isBureauDEtudes(user)) && user.entreprises.length) {
+    // un utilisateur entreprise
+    // ne voit que les utilisateurs de son entreprise
+    const entreprisesIds = user.entreprises.map(e => e.id)
+    if (entreprisesIds.every(eId => !(utilisateur.entreprise_ids ?? []).includes(eId))) {
+      return false
+    }
+  }
+
+  // On filtre en fonction des filtres sélectionnés
+  if (isNotNullNorUndefinedNorEmpty(params.administrationIds) && !params.administrationIds.includes(utilisateur.administration_id)) {
+    return false
+  }
+
+  if (
+    isNotNullNorUndefinedNorEmpty(params.entreprisesIds) &&
+    (isNullOrUndefinedOrEmpty(utilisateur.entreprise_ids) || utilisateur.entreprise_ids.every(eId => !(params.entreprisesIds ?? []).includes(eId)))
+  ) {
+    return false
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(params.roles) && !params.roles.includes(utilisateur.role)) {
+    return false
+  }
+
+  if (
+    isNotNullNorUndefinedNorEmpty(params.nomsUtilisateurs) &&
+    !utilisateur.nom.toLowerCase().includes(params.nomsUtilisateurs.toLowerCase()) &&
+    (isNullOrUndefinedOrEmpty(utilisateur.prenom) || !(utilisateur.prenom ?? '').toLowerCase().includes(params.nomsUtilisateurs.toLowerCase()))
+  ) {
+    return false
+  }
+
+  if (isNotNullNorUndefinedNorEmpty(params.emails) && !utilisateur.email.toLowerCase().includes(params.emails.toLowerCase())) {
+    return false
+  }
+
+  return true
+}
+
+const getUtilisateursDb = sql<Redefine<IGetUtilisateursDbQuery, undefined, GetUtilisateur>>`
+  select
+    id,
+    email,
+    nom,
+    prenom,
+    telephone_fixe,
+    telephone_mobile,
+    role,
+    administration_id,
+    (select array_agg(entreprise_id) from utilisateurs__entreprises where utilisateur_id = id) as entreprise_ids
+  from utilisateurs
+  where keycloak_id is not null`
+
+const emailValidator = z.object({ email: z.string() })
+export const getUtilisateursEmailsByEntrepriseIds = async (pool: Pool, entrepriseIds: NonEmptyArray<EntrepriseId>): Promise<string[]> => {
+  const result = await dbQueryAndValidate(getUtilisateursEmailsByEntrepriseIdsDb, { entrepriseIds }, pool, emailValidator)
+
+  return result.map(({ email }) => email)
+}
+
+const getUtilisateursEmailsByEntrepriseIdsDb = sql<Redefine<IGetUtilisateursEmailsByEntrepriseIdsDbQuery, { entrepriseIds: EntrepriseId[] }, z.infer<typeof emailValidator>>>`
+  select u.email from utilisateurs u join utilisateurs__entreprises ue on ue.utilisateur_id = u.id where ue.entreprise_id in $$entrepriseIds AND u.keycloak_id is not null`
diff --git a/packages/api/src/database/queries/utilisateurs.queries.types.ts b/packages/api/src/database/queries/utilisateurs.queries.types.ts
new file mode 100644
index 000000000..0c1ba75d1
--- /dev/null
+++ b/packages/api/src/database/queries/utilisateurs.queries.types.ts
@@ -0,0 +1,41 @@
+/** Types generated for queries found in "src/database/queries/utilisateurs.queries.ts" */
+export type stringArray = (string)[];
+
+/** 'GetUtilisateursDb' parameters type */
+export type IGetUtilisateursDbParams = void;
+
+/** 'GetUtilisateursDb' return type */
+export interface IGetUtilisateursDbResult {
+  administration_id: string | null;
+  email: string | null;
+  entreprise_ids: stringArray | null;
+  id: string;
+  nom: string | null;
+  prenom: string | null;
+  role: string;
+  telephone_fixe: string | null;
+  telephone_mobile: string | null;
+}
+
+/** 'GetUtilisateursDb' query type */
+export interface IGetUtilisateursDbQuery {
+  params: IGetUtilisateursDbParams;
+  result: IGetUtilisateursDbResult;
+}
+
+/** 'GetUtilisateursEmailsByEntrepriseIdsDb' parameters type */
+export interface IGetUtilisateursEmailsByEntrepriseIdsDbParams {
+  entrepriseIds: readonly (string | null | void)[];
+}
+
+/** 'GetUtilisateursEmailsByEntrepriseIdsDb' return type */
+export interface IGetUtilisateursEmailsByEntrepriseIdsDbResult {
+  email: string | null;
+}
+
+/** 'GetUtilisateursEmailsByEntrepriseIdsDb' query type */
+export interface IGetUtilisateursEmailsByEntrepriseIdsDbQuery {
+  params: IGetUtilisateursEmailsByEntrepriseIdsDbParams;
+  result: IGetUtilisateursEmailsByEntrepriseIdsDbResult;
+}
+
diff --git a/packages/api/src/database/queries/utilisateurs.test.integration.ts b/packages/api/src/database/queries/utilisateurs.test.integration.ts
new file mode 100644
index 000000000..8f9528845
--- /dev/null
+++ b/packages/api/src/database/queries/utilisateurs.test.integration.ts
@@ -0,0 +1,75 @@
+import { dbManager } from '../../../tests/db-manager'
+import { beforeAll, expect, afterAll, test, vi, describe } from 'vitest'
+import { getUtilisateursEmailsByEntrepriseIds } from './utilisateurs.queries'
+import { Pool } from 'pg'
+import { newEntrepriseId } from 'camino-common/src/entreprise'
+import { entrepriseUpsert } from './entreprises'
+import { utilisateurCreate } from './utilisateurs'
+import { newUtilisateurId } from '../models/_format/id-create'
+import { getCurrent } from 'camino-common/src/date'
+console.info = vi.fn()
+console.error = vi.fn()
+
+let dbPool: Pool
+
+beforeAll(async () => {
+  const { pool } = await dbManager.populateDb()
+  dbPool = pool
+})
+
+afterAll(async () => {
+  await dbManager.closeKnex()
+})
+
+describe('getUtilisateursEmailsByEntrepriseIds', () => {
+  test("renvoie une liste vide quand aucun utilisateur n'existe pour les entreprises demandées", async () => {
+    const result = await getUtilisateursEmailsByEntrepriseIds(dbPool, [newEntrepriseId('entrepriseId1')])
+
+    expect(result).toHaveLength(0)
+  })
+
+  test('récupère les emails des utilisateurs de plusieurs entreprises', async () => {
+    const entrepriseId = newEntrepriseId('entreprise_id1')
+    const entrepriseId2 = newEntrepriseId('entreprise_id2')
+    await entrepriseUpsert({
+      id: entrepriseId,
+      nom: 'Mon Entreprise',
+    })
+    await entrepriseUpsert({
+      id: entrepriseId2,
+      nom: 'Mon Entreprise',
+    })
+
+    const utilisateurId = newUtilisateurId('utilisateur_id')
+    await utilisateurCreate(
+      {
+        id: utilisateurId,
+        prenom: `jean`,
+        nom: `dupont`,
+        email: `jean@dupont.fr`,
+        dateCreation: getCurrent(),
+        keycloakId: 'iduser-keycloak',
+        role: 'entreprise',
+        entreprises: [{ id: entrepriseId }],
+      },
+      {}
+    )
+    await utilisateurCreate(
+      {
+        id: newUtilisateurId('utilisateur_id2'),
+        prenom: `antoine`,
+        nom: `dupont`,
+        email: `antoine@dupont.fr`,
+        dateCreation: getCurrent(),
+        keycloakId: 'iduser-keycloak',
+        role: 'entreprise',
+        entreprises: [{ id: entrepriseId2 }],
+      },
+      {}
+    )
+
+    const result = await getUtilisateursEmailsByEntrepriseIds(dbPool, [entrepriseId])
+    expect(result).toHaveLength(1)
+    expect(result[0]).toBe('jean@dupont.fr')
+  })
+})
diff --git a/packages/api/src/database/queries/utilisateurs.ts b/packages/api/src/database/queries/utilisateurs.ts
index ac098a696..2e71d2da9 100644
--- a/packages/api/src/database/queries/utilisateurs.ts
+++ b/packages/api/src/database/queries/utilisateurs.ts
@@ -1,17 +1,14 @@
-import { RawBuilder, QueryBuilder } from 'objection'
-
-import { IUtilisateursColonneId, IColonne, IUtilisateurTitre, IUtilisateur, formatUser } from '../../types'
+import { IUtilisateurTitre, IUtilisateur, formatUser } from '../../types'
 
 import options, { FieldsUtilisateur, FieldsUtilisateurTitre } from './_options'
 import graphBuild from './graph/build'
 import { fieldsFormat } from './graph/fields-format'
-import { stringSplit } from './_utils'
 
 import Utilisateurs from '../models/utilisateurs'
 import { utilisateursQueryModify } from './permissions/utilisateurs'
 import UtilisateursTitres from '../models/utilisateurs--titres'
-import { Role, User } from 'camino-common/src/roles'
-import { isNotNullNorUndefined, isNotNullNorUndefinedNorEmpty, isNullOrUndefined } from 'camino-common/src/typescript-tools'
+import { User } from 'camino-common/src/roles'
+import { DeepReadonly, isNotNullNorUndefinedNorEmpty, isNullOrUndefined } from 'camino-common/src/typescript-tools'
 
 const userGet = async (userId?: string): Promise<User> => {
   if (isNullOrUndefined(userId)) return null
@@ -36,7 +33,7 @@ const userGet = async (userId?: string): Promise<User> => {
   return undefined
 }
 
-const utilisateursQueryBuild = ({ fields }: { fields?: FieldsUtilisateur }, user: User) => {
+const utilisateursQueryBuild = ({ fields }: { fields?: FieldsUtilisateur }, user: DeepReadonly<User>) => {
   const graph = fields ? graphBuild(fields, 'utilisateur', fieldsFormat) : options.utilisateurs.graph
 
   const q = Utilisateurs.query().withGraphFetched(graph)
@@ -46,62 +43,6 @@ const utilisateursQueryBuild = ({ fields }: { fields?: FieldsUtilisateur }, user
   return q
 }
 
-const utilisateursFiltersQueryModify = (
-  {
-    ids,
-    entreprisesIds,
-    administrationIds,
-    roles,
-    noms,
-    emails,
-  }: {
-    ids?: string[]
-    entreprisesIds?: string[]
-    administrationIds?: string[]
-    roles?: Role[]
-    noms?: string | null
-    emails?: string | null
-  },
-  q: QueryBuilder<Utilisateurs, Utilisateurs[]>
-) => {
-  if (ids && ids.length > 0) {
-    q.whereIn('id', ids)
-  }
-
-  if (roles && roles.length > 0) {
-    q.whereIn('role', roles as string[])
-  }
-
-  if (administrationIds && administrationIds.length > 0) {
-    q.whereIn('administrationId', administrationIds)
-  }
-
-  if (entreprisesIds && entreprisesIds.length > 0) {
-    q.whereIn('entreprises.id', entreprisesIds).leftJoinRelated('entreprises')
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(noms)) {
-    const nomsArray = stringSplit(noms)
-    const fields = ['nom', 'prenom']
-
-    nomsArray.forEach(s => {
-      q.where(b => {
-        fields.forEach(f => {
-          b.orWhereRaw(`lower(??) like ?`, [`utilisateurs.${f}`, `%${s.toLowerCase()}%`])
-        })
-      })
-    })
-  }
-
-  if (isNotNullNorUndefinedNorEmpty(emails)) {
-    q.where(b => {
-      b.whereRaw(`LOWER(??) LIKE LOWER(?)`, ['utilisateurs.email', `%${emails}%`])
-    })
-  }
-
-  return q
-}
-
 const userByEmailGet = async (email: string | null | undefined): Promise<Utilisateurs | undefined> => {
   if (isNotNullNorUndefinedNorEmpty(email)) {
     const user: IUtilisateur | undefined = await Utilisateurs.query().withGraphFetched('[entreprises]').where('utilisateurs.email', email).first()
@@ -150,99 +91,6 @@ const utilisateurGet = async (id: string, { fields }: { fields?: FieldsUtilisate
   return q.findById(id)
 }
 
-// lien = administration ou entreprise(s) en relation avec l'utilisateur :
-// on trie sur la concaténation du nom de l'administration
-// avec l'aggrégation ordonnée(STRING_AGG) des noms des entreprises
-const utilisateursColonnes: Record<IUtilisateursColonneId, IColonne<string | RawBuilder>> = {
-  nom: { id: 'nom' },
-  prenom: { id: 'prenom' },
-  email: { id: 'email' },
-  role: { id: 'role' },
-}
-const utilisateursGet = async (
-  {
-    intervalle,
-    page,
-    colonne,
-    ordre,
-    ids,
-    entreprisesIds,
-    administrationIds,
-    roles,
-    noms,
-    emails,
-  }: {
-    intervalle?: number | null
-    page?: number | null
-    colonne?: IUtilisateursColonneId | null
-    ordre?: 'asc' | 'desc' | null
-    ids?: string[]
-    entreprisesIds?: string[]
-    administrationIds?: string[]
-    roles?: Role[]
-    noms?: string | null
-    emails?: string | null
-  },
-  { fields }: { fields?: FieldsUtilisateur } = {},
-  user: User
-) => {
-  const q = utilisateursQueryBuild({ fields }, user)
-
-  utilisateursFiltersQueryModify(
-    {
-      ids,
-      entreprisesIds,
-      administrationIds,
-      roles,
-      noms,
-      emails,
-    },
-    q
-  )
-
-  if (colonne) {
-    q.orderBy(utilisateursColonnes[colonne].id, ordre || 'asc')
-  } else {
-    q.orderBy('utilisateurs.nom', 'asc')
-  }
-
-  if (isNotNullNorUndefined(page) && page > 0 && isNotNullNorUndefined(intervalle) && intervalle > 0) {
-    q.offset((page - 1) * intervalle)
-  }
-
-  if (isNotNullNorUndefined(intervalle) && intervalle > 0) {
-    q.limit(intervalle)
-  }
-
-  return q
-}
-
-const utilisateursCount = async (
-  {
-    ids,
-    entreprisesIds,
-    administrationIds,
-    roles,
-    noms,
-    emails,
-  }: {
-    ids?: string[]
-    entreprisesIds?: string[]
-    administrationIds?: string[]
-    roles?: Role[]
-    noms?: string | null
-    emails?: string | null
-  },
-  { fields }: { fields?: FieldsUtilisateur },
-  user: User
-) => {
-  const q = utilisateursQueryBuild({ fields }, user)
-
-  utilisateursFiltersQueryModify({ ids, entreprisesIds, administrationIds, roles, noms, emails }, q)
-
-  return q.resultSize()
-}
-
 const utilisateurCreate = async (utilisateur: IUtilisateur, { fields }: { fields?: FieldsUtilisateur }) =>
   Utilisateurs.query()
     .insertGraph(utilisateur, options.utilisateurs.update)
@@ -260,4 +108,4 @@ const utilisateursTitresGet = async (titreId: string, { fields }: { fields?: Fie
     .where('titreId', titreId)
     .withGraphFetched(fields ? graphBuild(fields, 'utilisateursTitres', fieldsFormat) : options.utilisateursTitres.graph)
 
-export { userGet, utilisateurGet, userByEmailGet, utilisateursGet, utilisateursCount, utilisateurCreate, utilisateurUpsert, utilisateurTitreCreate, utilisateurTitreDelete, utilisateursTitresGet }
+export { userGet, utilisateurGet, userByEmailGet, utilisateurCreate, utilisateurUpsert, utilisateurTitreCreate, utilisateurTitreDelete, utilisateursTitresGet }
diff --git a/packages/api/src/server/rest.ts b/packages/api/src/server/rest.ts
index 976f55e97..d521d0e00 100644
--- a/packages/api/src/server/rest.ts
+++ b/packages/api/src/server/rest.ts
@@ -30,6 +30,7 @@ import {
   updateUtilisateurPermission,
   utilisateurs,
   registerToNewsletter,
+  getUtilisateurs,
 } from '../api/rest/utilisateurs'
 import { logout, resetPassword } from '../api/rest/keycloak'
 import { getDGTMStats, getGranulatsMarinsStats, getGuyaneStats, getMinerauxMetauxMetropolesStats } from '../api/rest/statistiques/index'
@@ -168,6 +169,7 @@ const restRouteImplementations: Readonly<{ [key in CaminoRestRoute]: Transform<k
   '/rest/utilisateurs/:id/permission': { postCall: updateUtilisateurPermission, ...CaminoRestRoutes['/rest/utilisateurs/:id/permission'] },
   '/rest/utilisateurs/:id/delete': { getCall: deleteUtilisateur, ...CaminoRestRoutes['/rest/utilisateurs/:id/delete'] },
   '/rest/utilisateurs/:id/newsletter': { getCall: isSubscribedToNewsletter, postCall: manageNewsletterSubscription, ...CaminoRestRoutes['/rest/utilisateurs/:id/newsletter'] }, // UNTESTED YET
+  '/rest/utilisateurs': { newGetCall: getUtilisateurs, ...CaminoRestRoutes['/rest/utilisateurs'] },
   '/rest/entreprises/:entrepriseId/fiscalite/:annee': { getCall: fiscalite, ...CaminoRestRoutes['/rest/entreprises/:entrepriseId/fiscalite/:annee'] }, // UNTESTED YET
   '/rest/entreprises/:entrepriseId': { getCall: getEntreprise, putCall: modifierEntreprise, ...CaminoRestRoutes['/rest/entreprises/:entrepriseId'] },
   '/rest/entreprises/:entrepriseId/documents': { getCall: getEntrepriseDocuments, postCall: postEntrepriseDocument, ...CaminoRestRoutes['/rest/entreprises/:entrepriseId/documents'] },
@@ -301,21 +303,26 @@ export const restWithPool = (dbPool: Pool): Router => {
                   Effect.mapError(caminoError => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR }))
                 )
               ),
+              Effect.tap(({ user }) => addLog(dbPool, user.id, 'post', req.url, req.body)),
               Effect.mapBoth({
                 onFailure: caminoError => {
                   console.warn(`problem with route ${route}: ${caminoError.message}`)
-                  res.status(caminoError.status).json(caminoError)
+
+                  if (!('status' in caminoError)) {
+                    res.status(HTTP_STATUS.INTERNAL_SERVER_ERROR).json(caminoError)
+                  } else {
+                    res.status(caminoError.status).json(caminoError)
+                  }
                 },
-                onSuccess: ({ parsedResult, user }) => {
+                onSuccess: ({ parsedResult }) => {
                   res.json(parsedResult)
-
-                  return addLog(dbPool, user.id, 'post', req.url, req.body)
                 },
               }),
               Effect.runPromiseExit
             )
 
             const pipeline = await call
+
             if (Exit.isFailure(pipeline)) {
               if (!Cause.isFailType(pipeline.cause)) {
                 console.error('catching error on newPost route', route, pipeline.cause, req.body)
diff --git a/packages/api/src/tools/api-mailjet/emails.ts b/packages/api/src/tools/api-mailjet/emails.ts
index ab43ead9c..250940ffa 100644
--- a/packages/api/src/tools/api-mailjet/emails.ts
+++ b/packages/api/src/tools/api-mailjet/emails.ts
@@ -3,6 +3,7 @@ import { mailjet } from './index'
 import { EmailTemplateId } from './types'
 import { emailCheck } from '../email-check'
 import { config } from '../../config/index'
+import { isNotNullNorUndefined, onlyUnique } from 'camino-common/src/typescript-tools'
 
 const from = {
   email: config().API_MAILJET_EMAIL,
@@ -21,6 +22,7 @@ export const mailjetSend = async (emails: string[], options: Record<string, any>
       }
     })
 
+    emails = emails.filter(isNotNullNorUndefined).filter(onlyUnique)
     // si on est pas sur le serveur de prod
     // l'adresse email du destinataire est remplacée
     if (config().NODE_ENV !== 'production' || config().ENV !== 'prod') {
diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts
index fe436d07d..e83c1adbc 100644
--- a/packages/api/src/types.ts
+++ b/packages/api/src/types.ts
@@ -91,7 +91,6 @@ type ITitreDemarcheColonneId = 'titreNom' | 'titreDomaine' | 'titreType' | 'titr
 
 type ITitreActiviteColonneId = 'titre' | 'titreDomaine' | 'titreType' | 'titreStatut' | 'titulaires' | 'annee' | 'periode' | 'statut'
 
-type IUtilisateursColonneId = 'nom' | 'prenom' | 'email' | 'role'
 type IEntrepriseColonneId = 'nom' | 'siren'
 
 interface IContenuId {
@@ -288,7 +287,7 @@ interface IUtilisateur {
   qgisToken?: string | null
 }
 
-export const formatUser = (userInBdd: IUtilisateur): UserNotNull => {
+export const formatUser = (userInBdd: Pick<IUtilisateur, 'email' | 'id' | 'nom' | 'prenom' | 'administrationId' | 'role' | 'entreprises'>): UserNotNull => {
   if (!isNotNullNorUndefined(userInBdd.email)) {
     throw new Error('l’email est obligatoire')
   }
@@ -377,7 +376,6 @@ export {
   ITitreColonneId,
   ITitreDemarcheColonneId,
   ITitreActiviteColonneId,
-  IUtilisateursColonneId,
   IEntrepriseColonneId,
   IColonne,
   IContenuId,
diff --git a/packages/api/tests/_utils/index.ts b/packages/api/tests/_utils/index.ts
index 42b65d412..53119ec4e 100644
--- a/packages/api/tests/_utils/index.ts
+++ b/packages/api/tests/_utils/index.ts
@@ -79,8 +79,9 @@ export const restNewCall = async <Route extends NewGetRestRoutes>(
   route: Route,
   params: CaminoRestParams<Route>,
   user: TestUser | undefined,
-  searchParams?: Record<string, string | string[]>
+  searchParams?: DeepReadonly<z.infer<(typeof CaminoRestRoutes)[Route]['newGet']['searchParams']>>
 ): Promise<request.Test> => {
+  // @ts-ignore
   const req = request(app(pool)).get(getRestRoute(route, params, searchParams))
 
   return jwtSet(req, user)
diff --git a/packages/common/src/permissions/utilisateurs.ts b/packages/common/src/permissions/utilisateurs.ts
index a9eace371..75c6ebf05 100644
--- a/packages/common/src/permissions/utilisateurs.ts
+++ b/packages/common/src/permissions/utilisateurs.ts
@@ -1,7 +1,8 @@
 import { isSuper, isAdministrationAdmin, isAdministrationEditeur, User, isAdministration, isEntreprise, isBureauDEtudes, ROLES, Role, UserNotNull } from '../roles'
+import { DeepReadonly } from '../typescript-tools'
 
 export const canCreateEntreprise = (user: User): boolean => isSuper(user) || isAdministrationAdmin(user) || isAdministrationEditeur(user)
-export const canReadUtilisateurs = (user: User): boolean => isSuper(user) || isAdministration(user) || isEntreprise(user) || isBureauDEtudes(user)
+export const canReadUtilisateurs = (user: DeepReadonly<User>): boolean => isSuper(user) || isAdministration(user) || isEntreprise(user) || isBureauDEtudes(user)
 
 export const canReadUtilisateur = (user: User, id: string): boolean => user?.id === id || canReadUtilisateurs(user)
 export const canDeleteUtilisateur = (user: User, id: string): boolean => {
diff --git a/packages/common/src/rest.ts b/packages/common/src/rest.ts
index 0597ac61f..14576cab7 100644
--- a/packages/common/src/rest.ts
+++ b/packages/common/src/rest.ts
@@ -12,7 +12,7 @@ import {
   entrepriseValidator,
 } from './entreprise'
 import { demarcheIdOrSlugValidator, demarcheIdValidator } from './demarche'
-import { newsletterAbonnementValidator, newsletterRegistrationValidator, qgisTokenValidator, utilisateurToEdit } from './utilisateur'
+import { newsletterAbonnementValidator, newsletterRegistrationValidator, qgisTokenValidator, utilisateurToEdit, utilisateursSearchParamsValidator, utilisateursTableValidator } from './utilisateur'
 import {
   editableTitreValidator,
   getDemarcheByIdOrSlugValidator,
@@ -79,6 +79,7 @@ const IDS = [
   '/rest/utilisateurs/:id/newsletter',
   '/rest/utilisateurs/:id/delete',
   '/rest/utilisateurs/:id/permission',
+  '/rest/utilisateurs',
   '/rest/statistiques/minerauxMetauxMetropole',
   '/rest/statistiques/guyane',
   '/rest/statistiques/guyane/:annee',
@@ -150,6 +151,7 @@ export const CaminoRestRoutes = {
   // On passe par un http get plutot qu'un http delete car nous terminons par une redirection vers la deconnexion de oauth2, qui se traduit mal sur certains navigateurs et essaie de faire un delete sur une route get
   '/rest/utilisateurs/:id/delete': { params: utilisateurIdParamsValidator, get: { output: z.void() } },
   '/rest/utilisateurs/:id/permission': { params: utilisateurIdParamsValidator, post: { input: utilisateurToEdit, output: z.void() } },
+  '/rest/utilisateurs': { params: noParamsValidator, newGet: { output: utilisateursTableValidator, searchParams: utilisateursSearchParamsValidator } },
   '/rest/statistiques/minerauxMetauxMetropole': { params: noParamsValidator, get: { output: statistiquesMinerauxMetauxMetropoleValidator } },
   '/rest/statistiques/guyane': { params: noParamsValidator, get: { output: statistiquesGuyaneDataValidator } },
   '/rest/statistiques/guyane/:annee': { params: z.object({ annee: caminoAnneeValidator }), get: { output: statistiquesGuyaneDataValidator } },
@@ -286,12 +288,12 @@ export const getRestRoute = <T extends CaminoRestRoute>(path: T, params: CaminoR
   const urlParams = new URLSearchParams()
   Object.keys(searchParams).forEach(key => {
     const params = searchParams[key]
-    if (typeof params === 'string') {
-      urlParams.append(key, params)
-    } else {
+    if (Array.isArray(params)) {
       for (const param of params) {
         urlParams.append(`${key}[]`, param)
       }
+    } else {
+      urlParams.append(key, params)
     }
   })
 
diff --git a/packages/common/src/roles.ts b/packages/common/src/roles.ts
index 0ba5bba17..a151c9281 100644
--- a/packages/common/src/roles.ts
+++ b/packages/common/src/roles.ts
@@ -46,7 +46,7 @@ type EntrepriseOrBureauDetudeRole = z.infer<typeof entrepriseRoleValidator>
 const entrepriseUserNotNullValidator = baseUserNotNullValidator.extend({ role: entrepriseRoleValidator, entreprises: z.array(entrepriseValidator.pick({ id: true })) })
 
 export type EntrepriseUserNotNull = z.infer<typeof entrepriseUserNotNullValidator>
-const userNotNullValidator = z.union([superUserNotNullValidator, defautUserNotNullValidator, adminUserNotNullValidator, entrepriseUserNotNullValidator])
+export const userNotNullValidator = z.discriminatedUnion('role', [superUserNotNullValidator, defautUserNotNullValidator, adminUserNotNullValidator, entrepriseUserNotNullValidator])
 export const userValidator = userNotNullValidator.nullable().optional()
 
 export const isSuper = (user: DeepReadonly<User>): user is UserSuper => userPermissionCheck(user, 'super')
diff --git a/packages/common/src/utilisateur.ts b/packages/common/src/utilisateur.ts
index 3afc3c578..f8e78e421 100644
--- a/packages/common/src/utilisateur.ts
+++ b/packages/common/src/utilisateur.ts
@@ -1,5 +1,5 @@
-import { Role, ROLES, utilisateurIdValidator } from './roles'
-import { AdministrationId, IDS } from './static/administrations'
+import { Role, ROLES, roleValidator, userNotNullValidator, utilisateurIdValidator } from './roles'
+import { AdministrationId, administrationIdValidator, IDS } from './static/administrations'
 import { z } from 'zod'
 import { entrepriseIdValidator } from './entreprise'
 
@@ -20,3 +20,39 @@ export const newsletterAbonnementValidator = z.object({ newsletter: z.boolean()
 export const newsletterRegistrationValidator = z.object({
   email: z.string(),
 })
+
+export const utilisateurValidator = userNotNullValidator.and(
+  z.object({
+    telephoneMobile: z.string().nullable(),
+    telephoneFixe: z.string().nullable(),
+  })
+)
+
+export type Utilisateur = z.infer<typeof utilisateurValidator>
+export const utilisateursTableValidator = z.object({
+  elements: z.array(utilisateurValidator),
+  total: z.number(),
+})
+export type UtilisateursTable = z.infer<typeof utilisateursTableValidator>
+
+const utilisateursColonneIdSortable = z.enum(['nom', 'prenom', 'email', 'role'])
+export type UtilisateursColonneIdSortable = z.infer<typeof utilisateursColonneIdSortable>
+
+const tableSearchParamsValidator = z.object({
+  page: z.coerce.number().optional().default(1),
+  intervalle: z.coerce.number().optional().default(10),
+  colonne: utilisateursColonneIdSortable.optional().default('nom'),
+  ordre: z.enum(['asc', 'desc']).optional().default('asc'),
+})
+
+export const utilisateursSearchParamsValidator = tableSearchParamsValidator.and(
+  z.object({
+    nomsUtilisateurs: z.string().optional(),
+    emails: z.string().optional(),
+    roles: z.array(roleValidator).optional(),
+    administrationIds: z.array(administrationIdValidator).optional(),
+    entreprisesIds: z.array(entrepriseIdValidator).optional(),
+  })
+)
+export type UtilisateursSearchParamsInput = (typeof utilisateursSearchParamsValidator)['_input']
+export type UtilisateursSearchParams = z.infer<typeof utilisateursSearchParamsValidator>
diff --git a/packages/ui/src/api/client-rest.ts b/packages/ui/src/api/client-rest.ts
index 319927363..77a0073f7 100644
--- a/packages/ui/src/api/client-rest.ts
+++ b/packages/ui/src/api/client-rest.ts
@@ -76,7 +76,7 @@ const callFetch = async <T extends CaminoRestRoute>(
   throw new CaminoHttpError(`Une erreur s'est produite lors de la récupération des données`, fetched.status as HttpStatus)
 }
 type GetWithJsonArgs<T extends GetRestRoutes | NewGetRestRoutes, Method extends keyof (typeof CaminoRestRoutes)[T]> = (typeof CaminoRestRoutes)[T][Method] extends { searchParams: ZodType }
-  ? [path: T, params: CaminoRestParams<T>, searchParams: z.infer<(typeof CaminoRestRoutes)[T][Method]['searchParams']>]
+  ? [path: T, params: CaminoRestParams<T>, searchParams: (typeof CaminoRestRoutes)[T][Method]['searchParams']['_input']]
   : [path: T, params: CaminoRestParams<T>]
 
 export const getWithJson = async <T extends GetRestRoutes>(...args: GetWithJsonArgs<T, 'get'>): Promise<z.infer<(typeof CaminoRestRoutes)[T]['get']['output']>> =>
diff --git a/packages/ui/src/components/_common/liste.tsx b/packages/ui/src/components/_common/liste.tsx
index 0465f573d..ebd847455 100644
--- a/packages/ui/src/components/_common/liste.tsx
+++ b/packages/ui/src/components/_common/liste.tsx
@@ -25,14 +25,19 @@ type ListeFiltreProps = {
   apiClient: Pick<ApiClient, 'titresRechercherByNom' | 'getTitresByIds'>
   entreprises: Entreprise[]
 }
-type Props<ColumnId extends string> = {
+
+type GetColumnId<ColumnWithId> = ColumnWithId extends { id: infer Id } ? Id : never
+
+type ToColumnId<List> = List extends [infer First, ...infer Rest] ? (First extends { noSort: true } ? ToColumnId<Rest> : [GetColumnId<First>, ...ToColumnId<Rest>]) : []
+
+type Props<ColumnId extends string, Columns> = {
   listeFiltre: ListeFiltreProps | null
-  colonnes: readonly Column<ColumnId>[]
-  getData: (params: Params<ColumnId>) => Promise<{ values: TableRow<ColumnId>[]; total: number }>
+  colonnes: Readonly<Columns>
+  getData: (params: Params<ToColumnId<Columns>[number]>) => Promise<{ values: TableRow<ColumnId>[]; total: number }>
   route: CaminoRouteLocation
 } & PageContentHeaderProps
 
-export const Liste = defineComponent(<ColumnId extends string>(props: Props<ColumnId>) => {
+export const Liste = defineComponent(<ColumnId extends string, Columns extends Column<ColumnId>[]>(props: Props<ColumnId, Columns>) => {
   const initialParams = getInitialParams(props.route, props.colonnes)
   const params = ref<Params<ColumnId>>({
     ...initialParams,
diff --git a/packages/ui/src/components/administration.stories.tsx b/packages/ui/src/components/administration.stories.tsx
index 6bd600631..489bc3229 100644
--- a/packages/ui/src/components/administration.stories.tsx
+++ b/packages/ui/src/components/administration.stories.tsx
@@ -23,6 +23,7 @@ export const Default: StoryFn = () => (
       role: 'super',
       ...testBlankUser,
     }}
+    entreprises={[]}
     apiClient={{
       administrationActivitesTypesEmails: (_: AdministrationId) =>
         Promise.resolve([
diff --git a/packages/ui/src/components/administration.tsx b/packages/ui/src/components/administration.tsx
index fd5b287d3..75aecd4a6 100644
--- a/packages/ui/src/components/administration.tsx
+++ b/packages/ui/src/components/administration.tsx
@@ -16,16 +16,18 @@ import { Region, Regions } from 'camino-common/src/static/region'
 import { computed, defineComponent, inject, onMounted, ref } from 'vue'
 import { AsyncData } from '@/api/client-rest'
 import { isNotNullNorUndefined } from 'camino-common/src/typescript-tools'
-import { userKey } from '@/moi'
+import { entreprisesKey, userKey } from '@/moi'
 import { capitalize } from 'camino-common/src/strings'
 import { Alert } from './_ui/alert'
 import { DsfrLink } from './_ui/dsfr-button'
 import { LabelWithValue } from './_ui/label-with-value'
+import { Entreprise } from 'camino-common/src/entreprise'
 
 export const Administration = defineComponent(() => {
   const route = useRoute<'administration'>()
 
   const user = inject(userKey)
+  const entreprises = inject(entreprisesKey, ref([]))
 
   const administrationId = computed<AdministrationId | null>(() => {
     if (isAdministrationId(route.params.id)) {
@@ -38,7 +40,7 @@ export const Administration = defineComponent(() => {
   return () => (
     <>
       {administrationId.value ? (
-        <PureAdministration administrationId={administrationId.value} user={user} apiClient={apiClient} />
+        <PureAdministration administrationId={administrationId.value} user={user} entreprises={entreprises.value} apiClient={apiClient} />
       ) : (
         <Alert title="Administration inconnue" type="error" small={true} />
       )}
@@ -49,6 +51,7 @@ export const Administration = defineComponent(() => {
 interface Props {
   administrationId: AdministrationId
   user: User
+  entreprises: Entreprise[]
   apiClient: Pick<ApiClient, 'administrationActivitesTypesEmails' | 'administrationUtilisateurs' | 'administrationActiviteTypeEmailUpdate' | 'administrationActiviteTypeEmailDelete'>
 }
 
@@ -167,7 +170,7 @@ export const PureAdministration = defineComponent<Props>(props => {
           data={utilisateurs.value}
           renderItem={item => (
             <div>
-              <TableAuto caption="Utilisateurs" columns={utilisateursColonnes} rows={utilisateursLignesBuild(item)} initialSort={'firstColumnAsc'} />
+              <TableAuto caption="Utilisateurs" columns={utilisateursColonnes} rows={utilisateursLignesBuild(item, props.entreprises)} initialSort={'firstColumnAsc'} />
             </div>
           )}
         />
@@ -208,4 +211,4 @@ export const PureAdministration = defineComponent<Props>(props => {
 })
 
 // @ts-ignore waiting for https://github.com/vuejs/core/issues/7833
-PureAdministration.props = ['administrationId', 'user', 'apiClient']
+PureAdministration.props = ['administrationId', 'user', 'entreprises', 'apiClient']
diff --git a/packages/ui/src/components/utilisateur/utilisateur-api-client.ts b/packages/ui/src/components/utilisateur/utilisateur-api-client.ts
index 93d241266..20acffa60 100644
--- a/packages/ui/src/components/utilisateur/utilisateur-api-client.ts
+++ b/packages/ui/src/components/utilisateur/utilisateur-api-client.ts
@@ -1,11 +1,10 @@
 import { apiGraphQLFetch } from '@/api/_client'
-import { EntrepriseId, Utilisateur } from 'camino-common/src/entreprise'
-import { QGISToken, UtilisateurToEdit } from 'camino-common/src/utilisateur'
+import { Utilisateur } from 'camino-common/src/entreprise'
+import { QGISToken, UtilisateurToEdit, UtilisateursSearchParamsInput, UtilisateursTable } from 'camino-common/src/utilisateur'
 
 import gql from 'graphql-tag'
 import { getWithJson, newGetWithJson, postWithJson } from '../../api/client-rest'
-import { Role, UtilisateurId } from 'camino-common/src/roles'
-import { AdministrationId } from 'camino-common/src/static/administrations'
+import { UtilisateurId } from 'camino-common/src/roles'
 
 export interface UtilisateurApiClient {
   getUtilisateur: (userId: UtilisateurId) => Promise<Utilisateur>
@@ -15,59 +14,12 @@ export interface UtilisateurApiClient {
   removeUtilisateur: (userId: UtilisateurId) => Promise<void>
   updateUtilisateur: (user: UtilisateurToEdit) => Promise<void>
   getQGISToken: () => Promise<QGISToken>
-  getUtilisateurs: (params: UtilisateursParams) => Promise<{ elements: Utilisateur[]; total: number }>
+  getUtilisateurs: (params: UtilisateursSearchParamsInput) => Promise<UtilisateursTable>
 }
 
-type UtilisateursParams = {
-  page?: number
-  colonne?: string
-  ordre?: 'asc' | 'desc'
-  noms?: string
-  emails?: string
-  roles?: Role[]
-  administrationIds?: AdministrationId[]
-  entreprisesIds?: EntrepriseId[]
-}
 export const utilisateurApiClient: UtilisateurApiClient = {
-  getUtilisateurs: async (params: UtilisateursParams) => {
-    const data = await apiGraphQLFetch(gql`
-      query Utilisateurs($page: Int, $colonne: String, $ordre: String, $entreprisesIds: [ID], $administrationIds: [ID], $roles: [ID], $noms: String, $emails: String) {
-        utilisateurs(
-          intervalle: 10
-          page: $page
-          colonne: $colonne
-          ordre: $ordre
-          entreprisesIds: $entreprisesIds
-          administrationIds: $administrationIds
-          roles: $roles
-          noms: $noms
-          emails: $emails
-        ) {
-          elements {
-            id
-            nom
-            prenom
-            email
-            telephoneMobile
-            telephoneFixe
-            entreprises {
-              id
-              nom
-              paysId
-              legalSiren
-              legalEtranger
-            }
-            administrationId
-            role
-          }
-          total
-        }
-      }
-    `)({
-      ...params,
-    })
-
-    return data
+  getUtilisateurs: async (params: UtilisateursSearchParamsInput) => {
+    return newGetWithJson('/rest/utilisateurs', {}, params)
   },
   getUtilisateur: async (userId: string) => {
     const data = await apiGraphQLFetch(gql`
diff --git a/packages/ui/src/components/utilisateurs.stories.tsx b/packages/ui/src/components/utilisateurs.stories.tsx
index 4e68e7329..0046e3833 100644
--- a/packages/ui/src/components/utilisateurs.stories.tsx
+++ b/packages/ui/src/components/utilisateurs.stories.tsx
@@ -40,6 +40,8 @@ const apiClientMock: Pick<ApiClient, 'getUtilisateurs' | 'titresRechercherByNom'
           nom: 'nom1',
           prenom: 'prenom1',
           role: 'super',
+          telephoneMobile: null,
+          telephoneFixe: null,
         },
         {
           id: toUtilisateurId('id2'),
@@ -48,6 +50,8 @@ const apiClientMock: Pick<ApiClient, 'getUtilisateurs' | 'titresRechercherByNom'
           prenom: 'prenom2',
           role: 'entreprise',
           entreprises: [entreprise],
+          telephoneMobile: null,
+          telephoneFixe: null,
         },
         {
           id: toUtilisateurId('id3'),
@@ -56,6 +60,8 @@ const apiClientMock: Pick<ApiClient, 'getUtilisateurs' | 'titresRechercherByNom'
           prenom: 'prenom3',
           role: 'admin',
           administrationId: 'aut-mrae-guyane-01',
+          telephoneMobile: null,
+          telephoneFixe: null,
         },
       ],
     })
diff --git a/packages/ui/src/components/utilisateurs.tsx b/packages/ui/src/components/utilisateurs.tsx
index 9b35fad69..011e2cba0 100644
--- a/packages/ui/src/components/utilisateurs.tsx
+++ b/packages/ui/src/components/utilisateurs.tsx
@@ -12,6 +12,7 @@ import { entreprisesKey, userKey } from '@/moi'
 import { Entreprise } from 'camino-common/src/entreprise'
 import { CaminoRouteLocation } from '@/router/routes'
 import { CaminoRouter } from '@/typings/vue-router'
+import { UtilisateursColonneIdSortable } from 'camino-common/src/utilisateur'
 
 interface Props {
   user: User
@@ -21,12 +22,12 @@ interface Props {
   entreprises: Entreprise[]
 }
 export const PureUtilisateurs = defineComponent<Props>(props => {
-  const load = async (params: Params<string>): Promise<{ values: TableRow[]; total: number }> => {
+  const load = async (params: Params<UtilisateursColonneIdSortable>): Promise<{ values: TableRow[]; total: number }> => {
     const getUtilisateursParams = {
       page: params.page,
       colonne: params.colonne,
       ordre: params.ordre,
-      noms: params.filtres?.nomsUtilisateurs,
+      nomsUtilisateurs: params.filtres?.nomsUtilisateurs,
       emails: params.filtres?.emails,
       roles: params.filtres?.roles,
       administrationIds: params.filtres?.administrationIds,
@@ -34,7 +35,7 @@ export const PureUtilisateurs = defineComponent<Props>(props => {
     }
     const utilisateurs = await props.apiClient.getUtilisateurs(getUtilisateursParams)
 
-    return { values: utilisateursLignesBuild(utilisateurs.elements), total: utilisateurs.total }
+    return { values: utilisateursLignesBuild(utilisateurs.elements, props.entreprises), total: utilisateurs.total }
   }
 
   return () => (
diff --git a/packages/ui/src/components/utilisateurs/table.ts b/packages/ui/src/components/utilisateurs/table.ts
index 5d5d36cea..4a1b27c2f 100644
--- a/packages/ui/src/components/utilisateurs/table.ts
+++ b/packages/ui/src/components/utilisateurs/table.ts
@@ -1,9 +1,9 @@
 import { List } from '../_ui/list'
-import { isAdministration, isBureauDEtudes, isEntreprise } from 'camino-common/src/roles'
+import { UserNotNull, isAdministration, isBureauDEtudes, isEntreprise } from 'camino-common/src/roles'
 import { Administrations } from 'camino-common/src/static/administrations'
 import { Column, ComponentColumnData, TableRow, TextColumnData } from '../_ui/table'
 import { markRaw } from 'vue'
-import { Utilisateur } from 'camino-common/src/entreprise'
+import { Entreprise, EntrepriseId } from 'camino-common/src/entreprise'
 
 export const utilisateursColonnes = [
   {
@@ -29,14 +29,20 @@ export const utilisateursColonnes = [
   },
 ] as const satisfies readonly Column[]
 
-export const utilisateursLignesBuild = (utilisateurs: Utilisateur[]): TableRow[] =>
-  utilisateurs.map(utilisateur => {
+export const utilisateursLignesBuild = (utilisateurs: UserNotNull[], entreprises: Entreprise[]): TableRow[] => {
+  const entreprisesIndex = entreprises.reduce<Record<EntrepriseId, Entreprise>>((acc, e) => {
+    acc[e.id] = e
+
+    return acc
+  }, {})
+
+  return utilisateurs.map(utilisateur => {
     let elements
 
     if (isAdministration(utilisateur)) {
       elements = [Administrations[utilisateur.administrationId].abreviation]
     } else if (isEntreprise(utilisateur) || isBureauDEtudes(utilisateur)) {
-      elements = utilisateur.entreprises?.map(({ nom }) => nom)
+      elements = utilisateur.entreprises?.map(({ id }) => entreprisesIndex[id].nom)
     }
 
     const lien: ComponentColumnData | TextColumnData =
@@ -69,3 +75,4 @@ export const utilisateursLignesBuild = (utilisateurs: Utilisateur[]): TableRow[]
       columns,
     }
   })
+}
-- 
GitLab