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

inclusion-numerique / coop-mediation-numerique / 39cc4768-bcb1-433f-a4a6-d76e5937751a

01 Apr 2026 04:06PM UTC coverage: 7.472% (+0.5%) from 6.94%
39cc4768-bcb1-433f-a4a6-d76e5937751a

push

circleci

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

MEP 2026-01-01

500 of 10542 branches covered (4.74%)

Branch coverage included in aggregate %.

145 of 414 new or added lines in 38 files covered. (35.02%)

13 existing lines in 10 files now uncovered.

1500 of 16224 relevant lines covered (9.25%)

36.99 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 { updateBrevoContact } from '@app/web/external-apis/brevo/updateBrevoContact'
4
import { deleteUser } from '@app/web/features/utilisateurs/use-cases/delete/deleteUser'
5
import { mergeUser } from '@app/web/features/utilisateurs/use-cases/merge/mergeUser'
6
import { nouveauReminders } from '@app/web/features/utilisateurs/use-cases/nouveau-reminders/nouveauReminders'
7
import { searchUser } from '@app/web/features/utilisateurs/use-cases/search/searchUser'
8
import { signupReminders } from '@app/web/features/utilisateurs/use-cases/signup-reminders/signupReminders'
9
import { updateUserFromDataspaceData } from '@app/web/features/utilisateurs/use-cases/update-from-dataspace/updateUserFromDataspaceData'
10
import { prismaClient } from '@app/web/prismaClient'
11
import {
12
  protectedProcedure,
13
  publicProcedure,
14
  router,
15
} from '@app/web/server/rpc/createRouter'
16
import { enforceIsAdmin } from '@app/web/server/rpc/enforceIsAdmin'
17
import { invalidError } from '@app/web/server/rpc/trpcErrors'
18
import { ChangeUserRolesValidation } from '@app/web/server/rpc/user/ChangeUserRolesValidation'
19
import { UserMergeValidation } from '@app/web/server/rpc/user/userMerge'
20
import { ServerUserSignupValidation } from '@app/web/server/rpc/user/userSignup.server'
21
import { addMutationLog } from '@app/web/utils/addMutationLog'
22
import { fixTelephone } from '@app/web/utils/clean-operations'
23
import { createStopwatch } from '@app/web/utils/stopwatch'
24
import { ProfilInscription } from '@prisma/client'
25
import { v4 } from 'uuid'
26
import { z } from 'zod'
27

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

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

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

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

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

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

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

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

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

132
        const stopwatch = createStopwatch()
×
133

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

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

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

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

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

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

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

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

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

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

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

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

281
        const rolesChanged =
NEW
282
          isMediateur !== currentIsMediateur ||
×
283
          isCoordinateur !== currentIsCoordinateur
NEW
284
        if (rolesChanged) {
×
NEW
285
          await updateBrevoContact(userId)
×
286
        }
287

UNCOV
288
        return updated
×
289
      },
290
    ),
291
  search: protectedProcedure
292
    .input(
293
      z.object({
294
        query: z.string(),
295
        includeDeleted: z.boolean().optional().default(false),
296
        excludeUserIds: z.array(z.string()).optional().default([]),
297
      }),
298
    )
299
    .query(
300
      ({
301
        input: { query, includeDeleted, excludeUserIds },
302
        ctx: { user: sessionUser },
303
      }) => {
304
        enforceIsAdmin(sessionUser)
×
305

306
        return searchUser({
×
307
          searchParams: {
308
            recherche: query,
309
          },
310
          includeDeleted,
311
          excludeUserIds,
312
        })
313
      },
314
    ),
315
  merge: protectedProcedure
316
    .input(UserMergeValidation)
317
    .mutation(
318
      async ({
319
        input: { sourceUserId, targetUserId },
320
        ctx: { user: sessionUser },
321
      }) => {
322
        enforceIsAdmin(sessionUser)
×
323

324
        return mergeUser(sourceUserId, targetUserId)
×
325
      },
326
    ),
327
  setFeatureFlags: protectedProcedure
328
    .input(UtilisateurSetFeatureFlagsValidation)
329
    .mutation(
330
      async ({
331
        input: { featureFlags, userId },
332
        ctx: { user: sessionUser },
333
      }) => {
334
        enforceIsAdmin(sessionUser)
×
335

336
        const user = await prismaClient.user.findUnique({
×
337
          where: {
338
            id: userId,
339
          },
340
          select: {
341
            id: true,
342
          },
343
        })
344
        if (!user) {
×
345
          throw invalidError('User not found')
×
346
        }
347

348
        const updated = await prismaClient.user.update({
×
349
          where: {
350
            id: userId,
351
          },
352
          data: {
353
            featureFlags,
354
          },
355
          select: {
356
            id: true,
357
            featureFlags: true,
358
          },
359
        })
360

361
        return updated
×
362
      },
363
    ),
364
  logoutUser: protectedProcedure
365
    .input(z.object({ userId: z.string().uuid() }))
366
    .mutation(async ({ input: { userId }, ctx: { user: sessionUser } }) => {
367
      enforceIsAdmin(sessionUser)
×
368

369
      const user = await prismaClient.user.findUnique({
×
370
        where: {
371
          id: userId,
372
        },
373
        select: {
374
          id: true,
375
        },
376
      })
377
      if (!user) {
×
378
        throw invalidError('User not found')
×
379
      }
380

381
      const { count: deletedSessionsCount } =
382
        await prismaClient.session.deleteMany({
×
383
          where: {
384
            userId,
385
          },
386
        })
387

388
      return {
×
389
        userId,
390
        deletedSessionsCount,
391
      }
392
    }),
393
  updateFromDataspace: protectedProcedure
394
    .input(z.object({ userId: z.string().uuid() }))
395
    .mutation(async ({ input: { userId }, ctx: { user: sessionUser } }) => {
396
      enforceIsAdmin(sessionUser)
×
397

398
      return updateUserFromDataspaceData({ userId })
×
399
    }),
400
})
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