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

inclusion-numerique / coop-mediation-numerique / 63ed3aef-04ec-416c-af7d-d56de15ebe3e

17 Mar 2026 03:48PM UTC coverage: 10.613% (-0.2%) from 10.79%
63ed3aef-04ec-416c-af7d-d56de15ebe3e

Pull #437

circleci

web-flow
Merge pull request #458 from inclusion-numerique/fix/admin-stats-min-date

fix(admin): allow filtering statistics from January 2020
Pull Request #437: release

685 of 10426 branches covered (6.57%)

Branch coverage included in aggregate %.

34 of 590 new or added lines in 86 files covered. (5.76%)

23 existing lines in 20 files now uncovered.

2130 of 16099 relevant lines covered (13.23%)

2.0 hits per line

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

0.0
/apps/web/src/server/rpc/mediateur/mediateursRouter.ts
1
import type { SessionUser } from '@app/web/auth/sessionUser'
2
import { isCoordinateur, isMediateur } from '@app/web/auth/userTypeGuards'
3
import { cancelInvitation } from '@app/web/equipe/cancelInvitation'
4
import { deleteFromArchive } from '@app/web/equipe/deleteFromArchive'
5
import { InvitationValidation } from '@app/web/equipe/InvitationValidation'
6
import { InviterMembreValidation } from '@app/web/equipe/InviterMembreValidation'
7
import { togglePartageStatistiques } from '@app/web/features/mediateurs/use-cases/partage-statistiques/db/togglePartageStatistiques'
8
import { acceptInvitation } from '@app/web/mediateurs/acceptInvitation'
9
import { addUserToTeam } from '@app/web/mediateurs/addUserToTeam'
10
import { declineInvitation } from '@app/web/mediateurs/declineInvitation'
11
import { findInvitationFrom } from '@app/web/mediateurs/findInvitationFrom'
12
import { inviteToJoinTeamOf } from '@app/web/mediateurs/inviteToJoinTeamOf'
13
import { leaveTeamOf } from '@app/web/mediateurs/leaveTeamOf'
14
import { removeMediateurFromTeamOf } from '@app/web/mediateurs/removeMediateurFromTeamOf'
15
import { resendInvitation } from '@app/web/mediateurs/resendInvitation'
16
import { searchMediateur } from '@app/web/mediateurs/searchMediateurs'
17
import { setVisibility } from '@app/web/mediateurs/setVisibility'
18
import { prismaClient } from '@app/web/prismaClient'
19
import {
20
  protectedProcedure,
21
  publicProcedure,
22
  router,
23
} from '@app/web/server/rpc/createRouter'
24
import { forbiddenError } from '@app/web/server/rpc/trpcErrors'
25
import { addMutationLog } from '@app/web/utils/addMutationLog'
26
import { createStopwatch } from '@app/web/utils/stopwatch'
27
import { z } from 'zod'
28

29
const assertAdminOrOwnerCoordinateur =
30
  (user: SessionUser) => (coordinateurId: string) => {
×
31
    if (user.role === 'Admin') return
×
32
    if (!isCoordinateur(user))
×
33
      throw forbiddenError('User is not a coordinateur')
×
34
    if (user.coordinateur.id !== coordinateurId)
×
35
      throw forbiddenError('Coordinateur mismatch')
×
36
  }
37

38
export const mediateursRouter = router({
×
39
  search: protectedProcedure
40
    .input(z.object({ query: z.string() }))
41
    .query(({ input: { query }, ctx: { user } }) => {
42
      if (!user.coordinateur && user.role !== 'Admin')
×
43
        throw forbiddenError('User is not a coordinateur')
×
44

45
      return searchMediateur({
×
46
        coordinateurId: user.coordinateur?.id,
47
        searchParams: { recherche: query },
48
      })
49
    }),
50
  removeFromTeam: protectedProcedure
51
    .input(
52
      z.object({
53
        mediateurId: z.string(),
54
        coordinateurId: z.string().optional(),
55
      }),
56
    )
57
    .mutation(
58
      async ({ input: { mediateurId, coordinateurId }, ctx: { user } }) => {
NEW
59
        const targetCoordinateurId = coordinateurId ?? user.coordinateur?.id
×
60

NEW
61
        if (!targetCoordinateurId) {
×
NEW
62
          throw forbiddenError('No coordinateur specified')
×
63
        }
64

NEW
65
        assertAdminOrOwnerCoordinateur(user)(targetCoordinateurId)
×
66

NEW
67
        const stopwatch = createStopwatch()
×
68

NEW
69
        await removeMediateurFromTeamOf({ id: targetCoordinateurId })(
×
70
          mediateurId,
71
        )
72

NEW
73
        addMutationLog({
×
74
          userId: user.id,
75
          nom: 'SupprimerMediateurCoordonne',
76
          duration: stopwatch.stop().duration,
77
          data: {
78
            coordinateurId: targetCoordinateurId,
79
            mediateurId,
80
          },
81
        })
82
      },
83
    ),
84
  leaveTeam: protectedProcedure
85
    .input(z.object({ coordinateurId: z.string() }))
86
    .mutation(async ({ input: { coordinateurId }, ctx: { user } }) => {
87
      if (!isMediateur(user)) throw forbiddenError('User is not a mediateur')
×
88

89
      const stopwatch = createStopwatch()
×
90

91
      await leaveTeamOf(user.mediateur)(coordinateurId)
×
92

93
      addMutationLog({
×
94
        userId: user.id,
95
        nom: 'SupprimerMediateurCoordonne',
96
        duration: stopwatch.stop().duration,
97
        data: {
98
          mediateurId: user.mediateur.id,
99
          coordinateurId,
100
        },
101
      })
102
    }),
103
  invite: protectedProcedure
104
    .input(InviterMembreValidation)
105
    .mutation(async ({ input: { members }, ctx: { user } }) => {
106
      if (!isCoordinateur(user))
×
107
        throw forbiddenError('User is not a coordinateur')
×
108

109
      const stopwatch = createStopwatch()
×
110

111
      await inviteToJoinTeamOf(user)(members)
×
112

113
      addMutationLog({
×
114
        userId: user.id,
115
        nom: 'InviterMediateursCoordonnes',
116
        duration: stopwatch.stop().duration,
117
        data: {
118
          coordinateurId: user.coordinateur.id,
119
          members,
120
        },
121
      })
122
    }),
123
  declineInvitation: publicProcedure
124
    .input(InvitationValidation)
125
    .mutation(async ({ input: { email, coordinateurId }, ctx: { user } }) => {
126
      const stopwatch = createStopwatch()
×
127

128
      const invitation = await findInvitationFrom(coordinateurId)(email)
×
129

130
      if (invitation == null)
×
131
        throw forbiddenError(
×
132
          'There is no invitation for this email matching coordinateurId',
133
        )
134

135
      await declineInvitation(invitation)
×
136

137
      addMutationLog({
×
138
        userId: user?.id ?? null,
×
139
        nom: 'RefuserInvitationMediateurCoordonne',
140
        duration: stopwatch.stop().duration,
141
        data: {
142
          email,
143
          coordinateurId,
144
        },
145
      })
146
    }),
147
  acceptInvitation: publicProcedure
148
    .input(InvitationValidation)
149
    .mutation(async ({ input: { email, coordinateurId }, ctx: { user } }) => {
150
      const stopwatch = createStopwatch()
×
151

152
      const invitation = await findInvitationFrom(coordinateurId)(email)
×
153

154
      if (invitation == null)
×
155
        throw forbiddenError(
×
156
          'There is no invitation for this email matching coordinateurId',
157
        )
158

159
      await acceptInvitation(invitation)
×
160

161
      addMutationLog({
×
162
        userId: user?.id ?? null,
×
163
        nom: 'AccepterInvitationMediateurCoordonne',
164
        duration: stopwatch.stop().duration,
165
        data: {
166
          email,
167
          coordinateurId,
168
        },
169
      })
170
    }),
171
  resendInvitation: protectedProcedure
172
    .input(z.object({ email: z.string(), coordinateurId: z.string() }))
173
    .mutation(async ({ input: { email, coordinateurId }, ctx: { user } }) => {
174
      assertAdminOrOwnerCoordinateur(user)(coordinateurId)
×
175

176
      const coordinateurUser = await prismaClient.user.findFirst({
×
177
        where: {
178
          coordinateur: {
179
            id: coordinateurId,
180
          },
181
        },
182
        select: {
183
          id: true,
184
          email: true,
185
          name: true,
186
        },
187
      })
188
      if (!coordinateurUser) throw forbiddenError('Coordinateur not found')
×
189

190
      const stopwatch = createStopwatch()
×
191

192
      await resendInvitation({
×
193
        email,
194
        coordinateurId,
195
        coordinateurName: coordinateurUser.name ?? 'Coordinateur', // fallback will never happen, here for type safety
×
196
      })
197

198
      addMutationLog({
×
199
        userId: user.id,
200
        nom: 'RenvoyerInvitationMediateurCoordonne' as const,
201
        duration: stopwatch.stop().duration,
202
        data: {
203
          email,
204
          coordinateurId,
205
        },
206
      })
207
    }),
208
  cancelInvitation: protectedProcedure
209
    .input(z.object({ email: z.string(), coordinateurId: z.string() }))
210
    .mutation(async ({ input: { email, coordinateurId }, ctx: { user } }) => {
211
      assertAdminOrOwnerCoordinateur(user)(coordinateurId)
×
212

213
      const stopwatch = createStopwatch()
×
214

215
      await cancelInvitation({
×
216
        email,
217
        coordinateurId,
218
      })
219

220
      addMutationLog({
×
221
        userId: user.id,
222
        nom: 'AnnulerInvitationMediateurCoordonne' as const,
223
        duration: stopwatch.stop().duration,
224
        data: {
225
          email,
226
          coordinateurId,
227
        },
228
      })
229
    }),
230
  deleteFromArchive: protectedProcedure
231
    .input(z.object({ mediateurId: z.string(), coordinateurId: z.string() }))
232
    .mutation(
233
      async ({ input: { mediateurId, coordinateurId }, ctx: { user } }) => {
234
        assertAdminOrOwnerCoordinateur(user)(coordinateurId)
×
235

236
        const stopwatch = createStopwatch()
×
237

238
        await deleteFromArchive({
×
239
          mediateurId,
240
          coordinateurId,
241
        })
242

243
        addMutationLog({
×
244
          userId: user.id,
245
          nom: 'SupprimerDefinitivementMediateurCoordonne' as const,
246
          duration: stopwatch.stop().duration,
247
          data: {
248
            mediateurId,
249
            coordinateurId,
250
          },
251
        })
252
      },
253
    ),
254
  addToTeam: protectedProcedure
255
    .input(
256
      z.object({
257
        userId: z.string().uuid(), // id du model User (pas Mediateur)
258
        coordinateurId: z.string().uuid(), // id du model Coordinateur (pas User)
259
      }),
260
    )
261
    .mutation(async ({ input: { userId, coordinateurId }, ctx: { user } }) => {
262
      // uniquement accessible par les admins
263
      if (user.role !== 'Admin') throw forbiddenError()
×
264

265
      return addUserToTeam({ userId, coordinateurId })
×
266
    }),
267
  setVisibility: protectedProcedure
268
    .input(z.object({ isVisible: z.boolean() }))
269
    .mutation(async ({ input: { isVisible }, ctx: { user } }) => {
270
      const stopwatch = createStopwatch()
×
271

272
      if (!isMediateur(user)) throw forbiddenError('User is not a mediateur')
×
273

274
      await setVisibility(user)(isVisible)
×
275

276
      addMutationLog({
×
277
        userId: user?.id ?? null,
×
278
        nom: 'SetMediateurVisibility',
279
        duration: stopwatch.stop().duration,
280
        data: {
281
          email: user.email,
282
          isVisible,
283
        },
284
      })
285
    }),
286
  shareStats: protectedProcedure.mutation(async ({ ctx: { user } }) => {
287
    const stopwatch = createStopwatch()
×
288

289
    if (!isMediateur(user) && !isCoordinateur(user))
×
290
      throw forbiddenError('User cannot share stats')
×
291

292
    const result = await togglePartageStatistiques(user)
×
293

294
    addMutationLog({
×
295
      userId: user?.id ?? null,
×
296
      nom: 'SetMediateurVisibility',
297
      duration: stopwatch.stop().duration,
298
      data: {
299
        email: user.email,
300
      },
301
    })
302

303
    return result?.active
×
304
  }),
305
})
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