• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

inclusion-numerique / coop-mediation-numerique / 134f1cc8-b272-471e-be1c-25fe3b4baafa

17 Mar 2026 04:15PM UTC coverage: 6.94% (-3.9%) from 10.79%
134f1cc8-b272-471e-be1c-25fe3b4baafa

push

circleci

web-flow
Merge pull request #437 from inclusion-numerique/dev

release

470 of 10426 branches covered (4.51%)

Branch coverage included in aggregate %.

28 of 584 new or added lines in 86 files covered. (4.79%)

1348 existing lines in 161 files now uncovered.

1355 of 15871 relevant lines covered (8.54%)

37.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/apps/web/src/server/rpc/user/userRouter.ts
1
import { UtilisateurSetFeatureFlagsValidation } from '@app/web/app/administration/utilisateurs/[id]/UtilisateurSetFeatureFlagsValidation'
2
import { UpdateProfileValidation } from '@app/web/app/user/UpdateProfileValidation'
3
import { deleteUser } from '@app/web/features/utilisateurs/use-cases/delete/deleteUser'
4
import { mergeUser } from '@app/web/features/utilisateurs/use-cases/merge/mergeUser'
5
import { nouveauReminders } from '@app/web/features/utilisateurs/use-cases/nouveau-reminders/nouveauReminders'
6
import { searchUser } from '@app/web/features/utilisateurs/use-cases/search/searchUser'
7
import { signupReminders } from '@app/web/features/utilisateurs/use-cases/signup-reminders/signupReminders'
8
import { updateUserFromDataspaceData } from '@app/web/features/utilisateurs/use-cases/update-from-dataspace/updateUserFromDataspaceData'
9
import { prismaClient } from '@app/web/prismaClient'
10
import {
11
  protectedProcedure,
12
  publicProcedure,
13
  router,
14
} from '@app/web/server/rpc/createRouter'
15
import { enforceIsAdmin } from '@app/web/server/rpc/enforceIsAdmin'
16
import { invalidError } from '@app/web/server/rpc/trpcErrors'
17
import { ChangeUserRolesValidation } from '@app/web/server/rpc/user/ChangeUserRolesValidation'
18
import { UserMergeValidation } from '@app/web/server/rpc/user/userMerge'
19
import { ServerUserSignupValidation } from '@app/web/server/rpc/user/userSignup.server'
20
import { addMutationLog } from '@app/web/utils/addMutationLog'
21
import { fixTelephone } from '@app/web/utils/clean-operations'
22
import { createStopwatch } from '@app/web/utils/stopwatch'
23
import { ProfilInscription } from '@prisma/client'
24
import { v4 } from 'uuid'
25
import { z } from 'zod'
26

27
export const userRouter = router({
×
28
  signup: publicProcedure
29
    .input(ServerUserSignupValidation)
30
    .mutation(({ input: { firstName, lastName, email } }) =>
31
      prismaClient.user.create({
×
32
        data: {
33
          id: v4(),
34
          firstName,
35
          lastName,
36
          name: `${firstName} ${lastName}`,
37
          email,
38
        },
39
        select: {
40
          id: true,
41
          email: true,
42
        },
43
      }),
44
    ),
45
  signupReminders: protectedProcedure.mutation(
46
    async ({ ctx: { user: sessionUser } }) => {
47
      enforceIsAdmin(sessionUser)
×
48

49
      await signupReminders()
×
50
    },
51
  ),
52
  inactiveReminders: protectedProcedure.mutation(
53
    async ({ ctx: { user: sessionUser } }) => {
54
      enforceIsAdmin(sessionUser)
×
55

56
      await nouveauReminders()
×
57
    },
58
  ),
59
  updateProfile: protectedProcedure
60
    .input(UpdateProfileValidation)
61
    .mutation(
62
      async ({ input: { firstName, lastName, phone }, ctx: { user } }) => {
63
        const stopwatch = createStopwatch()
×
64
        const updated = await prismaClient.user.update({
×
65
          where: { id: user.id },
66
          data: {
67
            firstName,
68
            lastName,
69
            phone: fixTelephone(phone ?? null),
×
70
            name: `${firstName} ${lastName}`,
71
          },
72
        })
73
        addMutationLog({
×
74
          userId: user.id,
75
          nom: 'ModifierUtilisateur',
76
          duration: stopwatch.stop().duration,
77
          data: {
78
            id: user.id,
79
            firstName,
80
            lastName,
81
            phone,
82
          },
83
        })
84
        return updated
×
85
      },
86
    ),
87
  deleteProfile: protectedProcedure.mutation(({ ctx: { user } }) =>
NEW
88
    deleteUser(user.id, user.email),
×
89
  ),
90
  adminDeleteUser: protectedProcedure
91
    .input(z.object({ userId: z.string().uuid() }))
92
    .mutation(async ({ input: { userId }, ctx: { user: sessionUser } }) => {
NEW
93
      enforceIsAdmin(sessionUser)
×
94

NEW
95
      const user = await prismaClient.user.findUnique({
×
96
        where: { id: userId },
97
        select: { id: true, email: true, role: true, deleted: true },
98
      })
99

NEW
100
      if (!user) throw invalidError('Utilisateur non trouvé')
×
101

NEW
102
      if (user.deleted) throw invalidError('Cet utilisateur est déjà supprimé')
×
103

NEW
104
      if (user.role === 'Admin' || user.role === 'Support') {
×
NEW
105
        throw invalidError(
×
106
          'Impossible de supprimer un administrateur ou support',
107
        )
108
      }
109

NEW
110
      return deleteUser(userId, user.email)
×
111
    }),
112
  markOnboardingAsSeen: protectedProcedure.mutation(({ ctx: { user } }) =>
113
    prismaClient.user.update({
×
114
      where: { id: user.id },
115
      data: { hasSeenOnboarding: new Date() },
116
    }),
117
  ),
118
  changeRoles: protectedProcedure
119
    .input(ChangeUserRolesValidation)
120
    .mutation(
121
      async ({
122
        input: { userId, isMediateur, isCoordinateur },
123
        ctx: { user: sessionUser },
124
      }) => {
NEW
125
        enforceIsAdmin(sessionUser)
×
126

NEW
127
        if (!isMediateur && !isCoordinateur) {
×
NEW
128
          throw invalidError('Au moins un rôle est requis')
×
129
        }
130

NEW
131
        const stopwatch = createStopwatch()
×
132

NEW
133
        const user = await prismaClient.user.findUnique({
×
134
          where: { id: userId, role: 'User' },
135
          select: {
136
            id: true,
137
            isConseillerNumerique: true,
138
            mediateur: {
139
              select: {
140
                id: true,
141
                beneficiairesCount: true,
142
                activitesCount: true,
143
              },
144
            },
145
            coordinateur: {
146
              select: {
147
                id: true,
148
                _count: {
149
                  select: {
150
                    mediateursCoordonnes: { where: { suppression: null } },
151
                  },
152
                },
153
              },
154
            },
155
          },
156
        })
157

NEW
158
        if (!user) {
×
NEW
159
          throw invalidError('User not found or user is admin')
×
160
        }
161

NEW
162
        const currentIsMediateur = !!user.mediateur
×
NEW
163
        const currentIsCoordinateur = !!user.coordinateur
×
164

165
        // Add mediateur role if needed
NEW
166
        if (isMediateur && !currentIsMediateur) {
×
NEW
167
          await prismaClient.mediateur.create({
×
168
            data: { userId },
169
          })
170
        }
171

172
        // Add coordinateur role if needed
NEW
173
        if (isCoordinateur && !currentIsCoordinateur) {
×
NEW
174
          await prismaClient.coordinateur.create({
×
175
            data: { userId },
176
          })
177
        }
178

179
        // Remove mediateur role if needed
NEW
180
        if (!isMediateur && currentIsMediateur && user.mediateur) {
×
NEW
181
          if (
×
182
            user.mediateur.beneficiairesCount > 0 ||
×
183
            user.mediateur.activitesCount > 0
184
          ) {
NEW
185
            throw invalidError(
×
186
              'Impossible de retirer le rôle médiateur : des bénéficiaires ou activités existent',
187
            )
188
          }
189

NEW
190
          const mediateurId = user.mediateur.id
×
NEW
191
          await prismaClient.$transaction([
×
192
            prismaClient.mediateurCoordonne.deleteMany({
193
              where: { mediateurId },
194
            }),
195
            prismaClient.mediateurEnActivite.deleteMany({
196
              where: { mediateurId },
197
            }),
198
            prismaClient.invitationEquipe.deleteMany({
199
              where: { mediateurId },
200
            }),
201
            prismaClient.tag.deleteMany({
202
              where: { mediateurId },
203
            }),
204
            prismaClient.partageStatistiques.deleteMany({
205
              where: { mediateurId },
206
            }),
207
            prismaClient.mediateur.delete({
208
              where: { id: mediateurId },
209
            }),
210
          ])
211
        }
212

213
        // Remove coordinateur role if needed
NEW
214
        if (!isCoordinateur && currentIsCoordinateur && user.coordinateur) {
×
NEW
215
          if (user.coordinateur._count.mediateursCoordonnes > 0) {
×
NEW
216
            throw invalidError(
×
217
              'Impossible de retirer le rôle coordinateur : des médiateurs sont encore coordonnés',
218
            )
219
          }
220

NEW
221
          const coordinateurId = user.coordinateur.id
×
NEW
222
          await prismaClient.$transaction([
×
223
            prismaClient.mediateurCoordonne.deleteMany({
224
              where: { coordinateurId },
225
            }),
226
            prismaClient.invitationEquipe.deleteMany({
227
              where: { coordinateurId },
228
            }),
229
            prismaClient.tag.deleteMany({
230
              where: { coordinateurId },
231
            }),
232
            prismaClient.activiteCoordination.deleteMany({
233
              where: { coordinateurId },
234
            }),
235
            prismaClient.partageStatistiques.deleteMany({
236
              where: { coordinateurId },
237
            }),
238
            prismaClient.coordinateur.delete({
239
              where: { id: coordinateurId },
240
            }),
241
          ])
242
        }
243

244
        // Update profilInscription based on resulting roles
NEW
245
        let profilInscription: ProfilInscription | null = null
×
NEW
246
        if (isMediateur && isCoordinateur) {
×
NEW
247
          profilInscription = user.isConseillerNumerique
×
248
            ? 'ConseillerNumerique'
249
            : 'Mediateur'
NEW
250
        } else if (isMediateur) {
×
NEW
251
          profilInscription = user.isConseillerNumerique
×
252
            ? 'ConseillerNumerique'
253
            : 'Mediateur'
NEW
254
        } else if (isCoordinateur) {
×
NEW
255
          profilInscription = user.isConseillerNumerique
×
256
            ? 'CoordinateurConseillerNumerique'
257
            : 'Coordinateur'
258
        }
259

NEW
260
        const updated = await prismaClient.user.update({
×
261
          where: { id: userId },
262
          data: {
263
            profilInscription,
264
          },
265
        })
266

NEW
267
        addMutationLog({
×
268
          userId,
269
          nom: 'ChangerRoles',
270
          duration: stopwatch.stop().duration,
271
          data: {
272
            id: userId,
273
            isMediateur,
274
            isCoordinateur,
275
            previousIsMediateur: currentIsMediateur,
276
            previousIsCoordinateur: currentIsCoordinateur,
277
          },
278
        })
279

NEW
280
        return updated
×
281
      },
282
    ),
283
  search: protectedProcedure
284
    .input(
285
      z.object({
286
        query: z.string(),
287
        includeDeleted: z.boolean().optional().default(false),
288
        excludeUserIds: z.array(z.string()).optional().default([]),
289
      }),
290
    )
291
    .query(
292
      ({
293
        input: { query, includeDeleted, excludeUserIds },
294
        ctx: { user: sessionUser },
295
      }) => {
296
        enforceIsAdmin(sessionUser)
×
297

298
        return searchUser({
×
299
          searchParams: {
300
            recherche: query,
301
          },
302
          includeDeleted,
303
          excludeUserIds,
304
        })
305
      },
306
    ),
307
  merge: protectedProcedure
308
    .input(UserMergeValidation)
309
    .mutation(
310
      async ({
311
        input: { sourceUserId, targetUserId },
312
        ctx: { user: sessionUser },
313
      }) => {
314
        enforceIsAdmin(sessionUser)
×
315

316
        return mergeUser(sourceUserId, targetUserId)
×
317
      },
318
    ),
319
  setFeatureFlags: protectedProcedure
320
    .input(UtilisateurSetFeatureFlagsValidation)
321
    .mutation(
322
      async ({
323
        input: { featureFlags, userId },
324
        ctx: { user: sessionUser },
325
      }) => {
326
        enforceIsAdmin(sessionUser)
×
327

328
        const user = await prismaClient.user.findUnique({
×
329
          where: {
330
            id: userId,
331
          },
332
          select: {
333
            id: true,
334
          },
335
        })
336
        if (!user) {
×
337
          throw invalidError('User not found')
×
338
        }
339

340
        const updated = await prismaClient.user.update({
×
341
          where: {
342
            id: userId,
343
          },
344
          data: {
345
            featureFlags,
346
          },
347
          select: {
348
            id: true,
349
            featureFlags: true,
350
          },
351
        })
352

353
        return updated
×
354
      },
355
    ),
356
  logoutUser: protectedProcedure
357
    .input(z.object({ userId: z.string().uuid() }))
358
    .mutation(async ({ input: { userId }, ctx: { user: sessionUser } }) => {
359
      enforceIsAdmin(sessionUser)
×
360

361
      const user = await prismaClient.user.findUnique({
×
362
        where: {
363
          id: userId,
364
        },
365
        select: {
366
          id: true,
367
        },
368
      })
369
      if (!user) {
×
370
        throw invalidError('User not found')
×
371
      }
372

373
      const { count: deletedSessionsCount } =
374
        await prismaClient.session.deleteMany({
×
375
          where: {
376
            userId,
377
          },
378
        })
379

380
      return {
×
381
        userId,
382
        deletedSessionsCount,
383
      }
384
    }),
385
  updateFromDataspace: protectedProcedure
386
    .input(z.object({ userId: z.string().uuid() }))
387
    .mutation(async ({ input: { userId }, ctx: { user: sessionUser } }) => {
388
      enforceIsAdmin(sessionUser)
×
389

390
      return updateUserFromDataspaceData({ userId })
×
391
    }),
392
})
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc