@@ -3,12 +3,12 @@ import { db } from '@sim/db'
33import { member , organization , session as sessionTable } from '@sim/db/schema'
44import { createLogger } from '@sim/logger'
55import { isOrgAdminRole } from '@sim/platform-authz/workspace'
6- import { and , eq , inArray , isNull , ne } from 'drizzle-orm'
6+ import { and , eq , inArray , isNull , ne , sql } from 'drizzle-orm'
77import { type NextRequest , NextResponse } from 'next/server'
88import { revokeOrganizationSessionsContract } from '@/lib/api/contracts/organization'
99import { parseRequest } from '@/lib/api/server'
1010import { getSession } from '@/lib/auth'
11- import { bumpSecurityPolicyVersion } from '@/lib/auth/security-policy'
11+ import { invalidateSecurityPolicyVersionCache } from '@/lib/auth/security-policy'
1212import { isOrganizationOnEnterprisePlan } from '@/lib/billing/core/subscription'
1313import { isBillingEnabled } from '@/lib/core/config/env-flags'
1414import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
@@ -78,25 +78,34 @@ export const POST = withRouteHandler(
7878 // real sessions so ending impersonation doesn't leave them signed out.
7979 const impersonatorId =
8080 ( session . session as { impersonatedBy ?: string | null } ) . impersonatedBy ?? null
81- const revoked = await db
82- . delete ( sessionTable )
83- . where (
84- and (
85- inArray (
86- sessionTable . userId ,
87- db
88- . select ( { userId : member . userId } )
89- . from ( member )
90- . where ( eq ( member . organizationId , organizationId ) )
91- ) ,
92- isNull ( sessionTable . impersonatedBy ) ,
93- ne ( sessionTable . token , session . session . token ) ,
94- ...( impersonatorId ? [ ne ( sessionTable . userId , impersonatorId ) ] : [ ] )
81+ // Delete and version bump commit atomically: a bump failure must roll the
82+ // delete back, or members would stay authenticated from the cookie cache
83+ // for up to 24h with their DB sessions already gone.
84+ const revoked = await db . transaction ( async ( tx ) => {
85+ const deleted = await tx
86+ . delete ( sessionTable )
87+ . where (
88+ and (
89+ inArray (
90+ sessionTable . userId ,
91+ tx
92+ . select ( { userId : member . userId } )
93+ . from ( member )
94+ . where ( eq ( member . organizationId , organizationId ) )
95+ ) ,
96+ isNull ( sessionTable . impersonatedBy ) ,
97+ ne ( sessionTable . token , session . session . token ) ,
98+ ...( impersonatorId ? [ ne ( sessionTable . userId , impersonatorId ) ] : [ ] )
99+ )
95100 )
96- )
97- . returning ( { id : sessionTable . id } )
98-
99- await bumpSecurityPolicyVersion ( organizationId )
101+ . returning ( { id : sessionTable . id } )
102+ await tx
103+ . update ( organization )
104+ . set ( { securityPolicyVersion : sql `${ organization . securityPolicyVersion } + 1` } )
105+ . where ( eq ( organization . id , organizationId ) )
106+ return deleted
107+ } )
108+ invalidateSecurityPolicyVersionCache ( organizationId )
100109
101110 logger . info ( 'Revoked organization sessions' , {
102111 organizationId,
0 commit comments