Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
Lets the founder open a target user's session in a new tab from the
panel for debugging. Read-only enforced server-side — any non-GET
request from an impersonated session returns 403.
Schema
- sessions.impersonated_by (uuid, nullable) — founder Better Auth user id
- sessions.impersonation_readonly (bool, default false)
- index on impersonated_by
Service
- ImpersonationService.createReadonlySession({ targetUserId, founderId,
ttlMinutes, reason, ipAddress, userAgent }):
- Random sessionId + token (32 bytes hex each)
- TTL clamped 1..60 min, default 15
- Refuses to impersonate admin users
- Inserts sessions row; signs cookie value with HMAC-SHA256(BETTER_AUTH_SECRET)
matching better-call's signCookieValue format
- Returns { cookieName, cookieValue, expiresAt, sessionId }
Guard
- ImpersonationReadonlyGuard runs after AuthGuard, before RolesGuard.
- GET/HEAD/OPTIONS pass through.
- For other methods: looks up sessions.impersonated_by + impersonation_readonly
by request.session.id; throws ForbiddenException if both truthy.
Endpoints (InternalAdminModule)
- POST /internal/admin/users/:id/impersonate-readonly [InternalTokenGuard]
body: { ttlMinutes, reason, founderId }
returns: { redirectUrl, expiresAt, sessionIdPrefix }
Hand-off is via signed consume URL (cross-origin Set-Cookie limitations).
- GET /admin/impersonate/consume?t=<signed> [@Public]
Verifies HMAC-signed payload (<=60s validity), sets the Better Auth session
cookie on sase.tr, redirects to /. One-shot.
Wiring
- InternalAdminModule imported in AppModule.
- ImpersonationReadonlyGuard registered as APP_GUARD between Auth and Roles.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3 lines
287 B
SQL
3 lines
287 B
SQL
ALTER TABLE "sessions" ADD COLUMN "impersonated_by" uuid;--> statement-breakpoint
|
|
ALTER TABLE "sessions" ADD COLUMN "impersonation_readonly" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
|
CREATE INDEX "sessions_impersonated_by_idx" ON "sessions" USING btree ("impersonated_by"); |