|
1 | 1 | import { redirect } from "@remix-run/server-runtime"; |
2 | | -import { $replica, prisma, type PrismaClientOrTransaction } from "~/db.server"; |
| 2 | +import { $replica, $transaction, prisma, type PrismaClientOrTransaction } from "~/db.server"; |
3 | 3 | import { logger } from "~/services/logger.server"; |
4 | 4 | import type { SearchParams } from "~/routes/admin._index"; |
5 | 5 | import { |
@@ -237,32 +237,42 @@ export async function redirectWithImpersonation( |
237 | 237 | const ipAddress = extractClientIp(xff); |
238 | 238 | const previousTargetId = await getImpersonationId(request); |
239 | 239 |
|
| 240 | + // Switching straight from one target to another never passes through `clearImpersonation`, so the |
| 241 | + // previous session is closed here, or the trail shows two overlapping STARTs. |
| 242 | + // |
| 243 | + // Both rows are written in one transaction: as separate statements, a failure between them could |
| 244 | + // start an impersonation whose only audit row is the STOP for the previous target — an admin |
| 245 | + // acting as someone with no record of it. |
| 246 | + // |
| 247 | + // `createdAt` is stamped explicitly rather than left to `@default(now())`, because Postgres `now()` |
| 248 | + // is the *transaction* timestamp: inside one transaction both rows would take the same value, and |
| 249 | + // an audit view ordered by that column couldn't tell which came first. |
| 250 | + const startedAt = new Date(); |
| 251 | + const closedAt = new Date(startedAt.getTime() - 1); |
| 252 | + |
240 | 253 | try { |
241 | | - // Switching straight from one target to another never passes through `clearImpersonation`, so |
242 | | - // close the previous session here or the trail shows two overlapping STARTs. |
243 | | - // |
244 | | - // Two statements rather than one `createMany`: `createdAt` defaults to `now()`, which is fixed |
245 | | - // for the duration of a statement, so a single insert would stamp both rows identically and an |
246 | | - // audit view ordered by `createdAt` couldn't tell which came first — the very ambiguity the |
247 | | - // STOP row exists to remove. |
248 | | - if (previousTargetId && previousTargetId !== userId) { |
249 | | - await prismaClient.impersonationAuditLog.create({ |
| 254 | + await $transaction(prismaClient, "startImpersonationAudit", async (tx) => { |
| 255 | + if (previousTargetId && previousTargetId !== userId) { |
| 256 | + await tx.impersonationAuditLog.create({ |
| 257 | + data: { |
| 258 | + action: "STOP", |
| 259 | + adminId: admin.id, |
| 260 | + targetId: previousTargetId, |
| 261 | + ipAddress, |
| 262 | + createdAt: closedAt, |
| 263 | + }, |
| 264 | + }); |
| 265 | + } |
| 266 | + |
| 267 | + await tx.impersonationAuditLog.create({ |
250 | 268 | data: { |
251 | | - action: "STOP", |
| 269 | + action: "START", |
252 | 270 | adminId: admin.id, |
253 | | - targetId: previousTargetId, |
| 271 | + targetId: userId, |
254 | 272 | ipAddress, |
| 273 | + createdAt: startedAt, |
255 | 274 | }, |
256 | 275 | }); |
257 | | - } |
258 | | - |
259 | | - await prismaClient.impersonationAuditLog.create({ |
260 | | - data: { |
261 | | - action: "START", |
262 | | - adminId: admin.id, |
263 | | - targetId: userId, |
264 | | - ipAddress, |
265 | | - }, |
266 | 276 | }); |
267 | 277 | } catch (error) { |
268 | 278 | logger.error("Failed to create impersonation audit log", { |
|
0 commit comments