Skip to content

Commit e56acf0

Browse files
Refactor sessions route: remove excess logging and extract helpers for clarity and maintainability.
🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent 3b3f1f8 commit e56acf0

File tree

1 file changed

+105
-55
lines changed

1 file changed

+105
-55
lines changed

web/src/app/api/sessions/route.ts

Lines changed: 105 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,104 @@
1-
import { NextResponse, NextRequest } from 'next/server'
2-
import { getServerSession } from 'next-auth'
31
import db from '@codebuff/common/db'
42
import * as schema from '@codebuff/common/db/schema'
53
import { 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+
77
import { 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

Comments
 (0)