1- import { NextResponse , NextRequest } from 'next/server'
2- import { getServerSession } from 'next-auth'
31import db from '@codebuff/common/db'
42import * as schema from '@codebuff/common/db/schema'
53import { and , eq , inArray } from 'drizzle-orm'
6- import { sha256 } from '@/lib/crypto'
4+ import { NextResponse } from 'next/server'
5+ import { getServerSession } from 'next-auth'
6+
77import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options'
8+ import { sha256 } from '@/lib/crypto'
9+ import { logger } from '@/util/logger'
10+
11+ import type { NextRequest } from 'next/server'
12+
13+ // Helper: revoke web/cli sessions for a user
14+ async function revokeStandardSessions (
15+ userId : string ,
16+ providedSessionIds : string [ ]
17+ ) {
18+ // Load user sessions (token, type, fingerprint)
19+ const userSessions = await db
20+ . select ( {
21+ sessionToken : schema . session . sessionToken ,
22+ type : schema . session . type ,
23+ fingerprintId : schema . session . fingerprint_id ,
24+ } )
25+ . from ( schema . session )
26+ . where ( eq ( schema . session . userId , userId ) )
27+
28+ // Map provided ids which may be raw tokens or sha256(token)
29+ const tokenSet = new Set ( userSessions . map ( ( s ) => s . sessionToken ) )
30+ const hashToToken = new Map (
31+ userSessions . map ( ( s ) => [ sha256 ( s . sessionToken ) , s . sessionToken ] as const )
32+ )
33+
34+ const tokensToDelete : string [ ] = [ ]
35+ for ( const provided of providedSessionIds ) {
36+ if ( tokenSet . has ( provided ) ) tokensToDelete . push ( provided )
37+ else {
38+ const mapped = hashToToken . get ( provided )
39+ if ( mapped ) tokensToDelete . push ( mapped )
40+ }
41+ }
42+
43+ if ( tokensToDelete . length === 0 ) return 0
44+
45+ // Restrict to web/cli sessions only
46+ const sessionsToDelete = userSessions . filter (
47+ ( s ) =>
48+ tokensToDelete . includes ( s . sessionToken ) &&
49+ ( s . type === 'web' || s . type === 'cli' )
50+ )
51+
52+ const cliFingerprintIds = Array . from (
53+ new Set (
54+ sessionsToDelete
55+ . filter ( ( s ) => s . type === 'cli' && s . fingerprintId )
56+ . map ( ( s ) => s . fingerprintId ! )
57+ )
58+ )
59+
60+ // Unclaim CLI fingerprints and delete sessions in a single transaction
61+ const deleted = await db . transaction ( async ( tx ) => {
62+ if ( cliFingerprintIds . length > 0 ) {
63+ await tx
64+ . update ( schema . fingerprint )
65+ . set ( { sig_hash : null } )
66+ . where ( inArray ( schema . fingerprint . id , cliFingerprintIds ) )
67+ }
68+
69+ const del = await tx
70+ . delete ( schema . session )
71+ . where (
72+ and (
73+ eq ( schema . session . userId , userId ) ,
74+ inArray ( schema . session . sessionToken , tokensToDelete ) ,
75+ // Explicitly restrict to web/cli to avoid PATs here
76+ inArray ( schema . session . type , [ 'web' , 'cli' ] as any )
77+ )
78+ )
79+ . returning ( { sessionToken : schema . session . sessionToken } )
80+
81+ return del . length
82+ } )
83+
84+ return deleted
85+ }
86+
87+ // Helper: revoke PAT tokens for a user
88+ async function revokeApiTokens ( userId : string , tokenIds : string [ ] ) {
89+ if ( ! tokenIds || tokenIds . length === 0 ) return 0
90+ const result = await db
91+ . delete ( schema . session )
92+ . where (
93+ and (
94+ eq ( schema . session . userId , userId ) ,
95+ eq ( schema . session . type , 'pat' ) ,
96+ inArray ( schema . session . sessionToken , tokenIds )
97+ )
98+ )
99+ . returning ( { sessionToken : schema . session . sessionToken } )
100+ return result . length
101+ }
8102
9103// DELETE /api/sessions
10104// Body: { sessionIds?: string[]; tokenIds?: string[] }
@@ -22,76 +116,32 @@ export async function DELETE(req: NextRequest) {
22116 . json ( )
23117 . catch ( ( ) => ( { } ) as any )
24118
119+ const userId = session . user . id
120+
25121 if (
26122 ( ! sessionIds || sessionIds . length === 0 ) &&
27123 ( ! tokenIds || tokenIds . length === 0 )
28124 ) {
29125 return NextResponse . json ( { revokedSessions : 0 , revokedTokens : 0 } )
30126 }
31127
32- const userId = session . user . id
33128 let revokedSessions = 0
34129 let revokedTokens = 0
35130
36- // 1) Map provided sessionIds (raw token or sha256(token)) to actual session tokens
37131 if ( sessionIds && sessionIds . length > 0 ) {
38- const userSessions = await db
39- . select ( {
40- sessionToken : schema . session . sessionToken ,
41- type : schema . session . type ,
42- } )
43- . from ( schema . session )
44- . where ( eq ( schema . session . userId , userId ) )
45-
46- const tokenSet = new Set ( userSessions . map ( ( s ) => s . sessionToken ) )
47- const hashToToken = new Map (
48- userSessions . map (
49- ( s ) => [ sha256 ( s . sessionToken ) , s . sessionToken ] as const
50- )
51- )
52-
53- const tokensToDelete : string [ ] = [ ]
54- for ( const provided of sessionIds ) {
55- if ( tokenSet . has ( provided ) ) {
56- tokensToDelete . push ( provided )
57- } else {
58- const mapped = hashToToken . get ( provided )
59- if ( mapped ) tokensToDelete . push ( mapped )
60- }
61- }
62-
63- if ( tokensToDelete . length > 0 ) {
64- const result = await db
65- . delete ( schema . session )
66- . where (
67- and (
68- eq ( schema . session . userId , userId ) ,
69- eq ( schema . session . type , 'web' ) , // do not delete PATs here
70- inArray ( schema . session . sessionToken , tokensToDelete )
71- )
72- )
73- . returning ( { sessionToken : schema . session . sessionToken } )
74- revokedSessions = result . length
75- }
132+ revokedSessions = await revokeStandardSessions ( userId , sessionIds )
76133 }
77134
78- // 2) Revoke API key tokens (PATs) by id (full token string)
79135 if ( tokenIds && tokenIds . length > 0 ) {
80- const result = await db
81- . delete ( schema . session )
82- . where (
83- and (
84- eq ( schema . session . userId , userId ) ,
85- eq ( schema . session . type , 'pat' ) ,
86- inArray ( schema . session . sessionToken , tokenIds )
87- )
88- )
89- . returning ( { sessionToken : schema . session . sessionToken } )
90- revokedTokens = result . length
136+ revokedTokens = await revokeApiTokens ( userId , tokenIds )
91137 }
92138
93139 return NextResponse . json ( { revokedSessions, revokedTokens } )
94140 } catch ( e : any ) {
141+ logger . error (
142+ { error : e ?. message ?? String ( e ) , stack : e ?. stack } ,
143+ 'Error in DELETE /api/sessions'
144+ )
95145 return new NextResponse ( e ?. message ?? 'Internal error' , { status : 500 } )
96146 }
97147}
0 commit comments