Skip to content

Commit c879dc0

Browse files
Refactor security section and standardize session terminology
This commit comprehensively refactors the security section component and API to improve code quality, performance, and consistency: **Component Improvements:** - Extract deleteSessions utility function outside component for better performance - Add useMemo for webSessions and cliSessions filtering to prevent unnecessary re-renders - Create proper Session TypeScript type definition for better type safety - Consolidate session logout logic with cleaner if/else structure - Remove redundant handleRevokeSession wrapper function **API Standardization:** - Convert sessionType from 'browser' to 'web' for consistency with database schema - Use database session type directly instead of converting to 'browser' - Move PAT filtering to SQL query for better performance **Benefits:** - Better React performance with memoization - Improved type safety and maintainability - Consistent terminology across frontend, backend, and database - Cleaner, more readable code structure 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent 1dbbb7f commit c879dc0

File tree

2 files changed

+72
-59
lines changed

2 files changed

+72
-59
lines changed

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { NextResponse } from 'next/server'
2-
import { sha256 } from '@/lib/crypto'
31
import db from '@codebuff/common/db'
42
import * as schema from '@codebuff/common/db/schema'
5-
import { eq } from 'drizzle-orm'
6-
import { getServerSession } from 'next-auth'
3+
import { eq, and, not } from 'drizzle-orm/expressions'
74
import { cookies } from 'next/headers'
5+
import { NextResponse } from 'next/server'
6+
import { getServerSession } from 'next-auth'
87

98
import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options'
9+
import { sha256 } from '@/lib/crypto'
1010

1111
function getCurrentSessionTokenFromCookies(): string | null {
1212
const jar = cookies()
@@ -39,7 +39,12 @@ export async function GET() {
3939
schema.fingerprint,
4040
eq(schema.session.fingerprint_id, schema.fingerprint.id)
4141
)
42-
.where(eq(schema.session.userId, session.user.id))
42+
.where(
43+
and(
44+
eq(schema.session.userId, session.user.id),
45+
not(eq(schema.session.type, 'pat'))
46+
)
47+
)
4348

4449
const currentToken = getCurrentSessionTokenFromCookies()
4550

@@ -50,11 +55,6 @@ export async function GET() {
5055
const token = r.sessionToken
5156
const label = token ? `••••${token.slice(-4)}` : '••••'
5257

53-
// Skip PATs - they are handled by the /api/api-keys endpoint
54-
if (r.type === 'pat') {
55-
continue
56-
}
57-
5858
// All non-PAT sessions are now unified as 'web' type
5959
activeSessions.push({
6060
id: sha256(token),
@@ -63,7 +63,7 @@ export async function GET() {
6363
isCurrent: token === currentToken,
6464
fingerprintId: r.fingerprint_id,
6565
createdAt: r.fingerprintCreatedAt?.toISOString() ?? null,
66-
sessionType: r.fingerprint_id ? 'cli' : 'browser', // For display purposes only
66+
sessionType: r.type,
6767
})
6868
}
6969

web/src/app/profile/components/security-section.tsx

Lines changed: 61 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
'use client'
22

3-
import { useState } from 'react'
43
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
4+
import { Monitor, Terminal } from 'lucide-react'
5+
import { useState, useMemo } from 'react'
6+
7+
import { Badge } from '@/components/ui/badge'
58
import { Button } from '@/components/ui/button'
9+
import { ConfirmationInputDialog } from '@/components/ui/confirmation-input-dialog'
610
import {
711
Table,
812
TableBody,
@@ -12,22 +16,37 @@ import {
1216
TableRow,
1317
} from '@/components/ui/table'
1418
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
15-
import { Badge } from '@/components/ui/badge'
16-
import { ConfirmationInputDialog } from '@/components/ui/confirmation-input-dialog'
17-
import { Monitor, Terminal } from 'lucide-react'
1819
import { useToast } from '@/components/ui/use-toast'
20+
1921
import { ProfileSection } from './profile-section'
2022

23+
type Session = {
24+
id: string
25+
label?: string
26+
expires?: string | null
27+
isCurrent?: boolean
28+
fingerprintId?: string | null
29+
createdAt?: string | null
30+
sessionType?: 'web' | 'cli'
31+
}
32+
33+
// Utility function to delete sessions via API
34+
async function deleteSessions(sessionIds: string[]): Promise<void> {
35+
if (sessionIds.length === 0) return
36+
37+
const res = await fetch('/api/sessions', {
38+
method: 'DELETE',
39+
headers: { 'Content-Type': 'application/json' },
40+
body: JSON.stringify({ sessionIds }),
41+
})
42+
43+
if (!res.ok) {
44+
throw new Error(await res.text())
45+
}
46+
}
47+
2148
async function fetchSessions(): Promise<{
22-
activeSessions: {
23-
id: string
24-
label?: string
25-
expires?: string | null
26-
isCurrent?: boolean
27-
fingerprintId?: string | null
28-
createdAt?: string | null
29-
sessionType?: 'browser' | 'cli'
30-
}[]
49+
activeSessions: Session[]
3150
}> {
3251
const res = await fetch('/api/user/sessions')
3352
if (!res.ok) throw new Error(await res.text())
@@ -50,19 +69,23 @@ export function SecuritySection() {
5069
})
5170

5271
const allSessions = sessionsData?.activeSessions ?? []
53-
const webSessions = allSessions.filter(
54-
(session) => session.sessionType === 'browser'
72+
73+
const webSessions = useMemo(
74+
() => allSessions.filter((session) => session.sessionType === 'web'),
75+
[allSessions]
5576
)
56-
const cliSessions = allSessions.filter(
57-
(session) => session.sessionType === 'cli'
77+
78+
const cliSessions = useMemo(
79+
() => allSessions.filter((session) => session.sessionType === 'cli'),
80+
[allSessions]
5881
)
5982

6083
const [activeTab, setActiveTab] = useState<'web' | 'cli'>('web')
6184
const [isLogoutConfirmOpen, setIsLogoutConfirmOpen] = useState(false)
6285
const [isBulkLoggingOut, setIsBulkLoggingOut] = useState(false)
6386

6487
const TAB_LABELS = { web: 'Web Sessions', cli: 'CLI Sessions' } as const
65-
const PRIMARY_VERB = { web: 'Log out of all', cli: 'Revoke all' } as const
88+
const PRIMARY_VERB = { web: 'Log out of other', cli: 'Revoke all' } as const
6689
const CONFIRM_VERB = { web: 'Log Out', cli: 'Revoke' } as const
6790

6891
const revokeSessionMutation = useMutation({
@@ -88,42 +111,28 @@ export function SecuritySection() {
88111
},
89112
})
90113

91-
async function handleRevokeSession(id: string) {
92-
revokeSessionMutation.mutate(id)
93-
}
94-
95114
async function handleLogoutAll() {
96115
setIsLogoutConfirmOpen(true)
97116
}
98-
99-
async function confirmLogoutAll() {
117+
async function confirmLogoutAll(): Promise<void> {
100118
try {
101119
setIsBulkLoggingOut(true)
120+
121+
let sessionsToLogout: Session[]
122+
let toastMessage: string
123+
102124
if (activeTab === 'web') {
103-
const sessionIds = webSessions
104-
.filter((s) => !s.isCurrent)
105-
.map((s) => s.id)
106-
if (sessionIds.length > 0) {
107-
const res = await fetch('/api/sessions', {
108-
method: 'DELETE',
109-
headers: { 'Content-Type': 'application/json' },
110-
body: JSON.stringify({ sessionIds }),
111-
})
112-
if (!res.ok) throw new Error(await res.text())
113-
}
114-
toast({ title: 'Logged out of all web sessions' })
125+
sessionsToLogout = webSessions.filter((s) => !s.isCurrent)
126+
toastMessage = 'Logged out of other web sessions'
115127
} else {
116-
const sessionIds = cliSessions.map((s) => s.id)
117-
if (sessionIds.length > 0) {
118-
const res = await fetch('/api/sessions', {
119-
method: 'DELETE',
120-
headers: { 'Content-Type': 'application/json' },
121-
body: JSON.stringify({ sessionIds }),
122-
})
123-
if (!res.ok) throw new Error(await res.text())
124-
}
125-
toast({ title: 'Revoked all CLI sessions' })
128+
sessionsToLogout = cliSessions
129+
toastMessage = 'Revoked all CLI sessions'
126130
}
131+
132+
const sessionIds = sessionsToLogout.map((s) => s.id)
133+
await deleteSessions(sessionIds)
134+
toast({ title: toastMessage })
135+
127136
await queryClient.invalidateQueries({ queryKey: ['sessions'] })
128137
setIsLogoutConfirmOpen(false)
129138
} catch (e: any) {
@@ -262,7 +271,9 @@ export function SecuritySection() {
262271
<Button
263272
size="sm"
264273
variant="destructive"
265-
onClick={() => handleRevokeSession(s.id)}
274+
onClick={() => {
275+
revokeSessionMutation.mutate(s.id)
276+
}}
266277
>
267278
Revoke
268279
</Button>
@@ -331,7 +342,9 @@ export function SecuritySection() {
331342
<Button
332343
size="sm"
333344
variant="destructive"
334-
onClick={() => handleRevokeSession(s.id)}
345+
onClick={() => {
346+
revokeSessionMutation.mutate(s.id)
347+
}}
335348
>
336349
Revoke
337350
</Button>

0 commit comments

Comments
 (0)