Skip to content

Commit 2570e8c

Browse files
authored
Merge pull request #38 from ITSEC-Research/yoko-dev
feat: improve request handling with abort-aware error management and enhance user preferences UI/UX
2 parents f096260 + 8d226d9 commit 2570e8c

File tree

15 files changed

+662
-74
lines changed

15 files changed

+662
-74
lines changed

app/api/domain-recon/credentials/route.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import { NextRequest, NextResponse } from "next/server"
22
import { executeQuery as executeClickHouseQuery } from "@/lib/clickhouse"
33
import { validateRequest } from "@/lib/auth"
4+
import { throwIfAborted, getRequestSignal, handleAbortError } from "@/lib/api-helpers"
45

56
export async function POST(request: NextRequest) {
7+
// ✅ Check abort VERY EARLY - before validateRequest
8+
throwIfAborted(request)
9+
610
const user = await validateRequest(request)
711
if (!user) {
812
return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 })
913
}
1014

1115
try {
16+
// Check if request was aborted early
17+
throwIfAborted(request)
18+
1219
const body = await request.json()
1320
const { targetDomain, filters, pagination, searchQuery, searchType = 'domain' } = body
1421

@@ -27,8 +34,17 @@ export async function POST(request: NextRequest) {
2734
}
2835
console.log("🚀 API Called (Optimized):", logData)
2936

37+
// Get signal for passing to database queries
38+
const signal = getRequestSignal(request)
39+
40+
// Check abort before expensive operations
41+
throwIfAborted(request)
42+
3043
// Call the new data getter function
31-
const credentialsData = await getCredentialsDataOptimized(cleanDomain, filters, pagination, searchQuery, searchType, body.keywordMode)
44+
const credentialsData = await getCredentialsDataOptimized(cleanDomain, filters, pagination, searchQuery, searchType, body.keywordMode, signal)
45+
46+
// Check abort after operations
47+
throwIfAborted(request)
3248

3349
return NextResponse.json({
3450
success: true,
@@ -39,6 +55,13 @@ export async function POST(request: NextRequest) {
3955
})
4056

4157
} catch (error) {
58+
// Handle abort errors gracefully
59+
const abortResponse = handleAbortError(error)
60+
if (abortResponse) {
61+
return abortResponse
62+
}
63+
64+
// Handle other errors
4265
console.error("❌ Error in credentials API:", error)
4366
return NextResponse.json(
4467
{ error: "Failed to get credentials data", details: error instanceof Error ? error.message : "Unknown error" },
@@ -53,7 +76,8 @@ async function getCredentialsDataOptimized(
5376
pagination?: any,
5477
searchQuery?: string,
5578
searchType: 'domain' | 'keyword' = 'domain',
56-
keywordMode: 'domain-only' | 'full-url' = 'full-url'
79+
keywordMode: 'domain-only' | 'full-url' = 'full-url',
80+
signal?: AbortSignal
5781
) {
5882
// SECURITY: Validate and sanitize pagination parameters
5983
const page = Math.max(1, Math.floor(Number(pagination?.page)) || 1)
@@ -192,7 +216,7 @@ async function getCredentialsDataOptimized(
192216
`
193217
}
194218

195-
const countResult = (await executeClickHouseQuery(countQuery, params)) as any[]
219+
const countResult = (await executeClickHouseQuery(countQuery, params, signal)) as any[]
196220
const total = countResult[0]?.total || 0
197221

198222
// ==========================================
@@ -237,7 +261,7 @@ async function getCredentialsDataOptimized(
237261

238262
console.log("📊 Data Query Executing with PREWHERE...")
239263

240-
const data = (await executeClickHouseQuery(dataQuery, params)) as any[]
264+
const data = (await executeClickHouseQuery(dataQuery, params, signal)) as any[]
241265

242266
return {
243267
data: data.map((row: any) => ({

app/api/domain-recon/overview/route.ts

Lines changed: 70 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextRequest, NextResponse } from "next/server"
22
import { executeQuery as executeClickHouseQuery } from "@/lib/clickhouse"
33
import { validateRequest } from "@/lib/auth"
4+
import { throwIfAborted, getRequestSignal, handleAbortError } from "@/lib/api-helpers"
45

56
/**
67
* Build WHERE clause for domain matching (ClickHouse version)
@@ -56,19 +57,28 @@ function buildKeywordWhereClause(keyword: string, mode: 'domain-only' | 'full-ur
5657
}
5758

5859
export async function POST(request: NextRequest) {
60+
// ✅ Check abort VERY EARLY - before validateRequest
61+
throwIfAborted(request)
62+
5963
const user = await validateRequest(request)
6064
if (!user) {
6165
return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 })
6266
}
6367

6468
try {
69+
// Check if request was aborted early
70+
throwIfAborted(request)
71+
6572
const body = await request.json()
6673
const { targetDomain, timelineGranularity, searchType = 'domain', type } = body
6774

6875
if (!targetDomain || typeof targetDomain !== 'string') {
6976
return NextResponse.json({ error: "targetDomain is required" }, { status: 400 })
7077
}
7178

79+
// Get signal for passing to database queries
80+
const signal = getRequestSignal(request)
81+
7282
let whereClause = ''
7383
let params: Record<string, string> = {}
7484

@@ -97,17 +107,28 @@ export async function POST(request: NextRequest) {
97107
// ============================================
98108

99109
if (type === 'stats') {
110+
// Check abort before expensive operations
111+
throwIfAborted(request)
112+
100113
// Fast data: Subdomains + Paths only
101114
const [topSubdomains, topPaths] = await Promise.all([
102-
getTopSubdomains(whereClause, params, 10, searchType, body.keywordMode || 'full-url', targetDomain).catch((e) => {
103-
console.error("❌ Subdomains Error:", e)
115+
getTopSubdomains(whereClause, params, 10, searchType, body.keywordMode || 'full-url', targetDomain, signal).catch((e) => {
116+
// Don't log AbortError as error
117+
if (e instanceof Error && e.name !== 'AbortError') {
118+
console.error("❌ Subdomains Error:", e)
119+
}
104120
return []
105121
}),
106-
getTopPaths(whereClause, params, 10).catch((e) => {
107-
console.error("❌ Paths Error:", e)
122+
getTopPaths(whereClause, params, 10, signal).catch((e) => {
123+
if (e instanceof Error && e.name !== 'AbortError') {
124+
console.error("❌ Paths Error:", e)
125+
}
108126
return []
109127
}),
110128
])
129+
130+
// Check abort after operations
131+
throwIfAborted(request)
111132

112133
console.log("✅ Stats data retrieved:", {
113134
topSubdomainsCount: topSubdomains?.length || 0,
@@ -124,11 +145,19 @@ export async function POST(request: NextRequest) {
124145
}
125146

126147
if (type === 'timeline') {
148+
// Check abort before expensive operations
149+
throwIfAborted(request)
150+
127151
// Slow data: Timeline only
128-
const timelineData = await getTimelineData(whereClause, params, timelineGranularity || 'auto').catch((e) => {
129-
console.error("❌ Timeline Error:", e)
152+
const timelineData = await getTimelineData(whereClause, params, timelineGranularity || 'auto', signal).catch((e) => {
153+
if (e instanceof Error && e.name !== 'AbortError') {
154+
console.error("❌ Timeline Error:", e)
155+
}
130156
return []
131157
})
158+
159+
// Check abort after operations
160+
throwIfAborted(request)
132161

133162
console.log("✅ Timeline data retrieved:", {
134163
timelineCount: timelineData?.length || 0,
@@ -144,20 +173,32 @@ export async function POST(request: NextRequest) {
144173
}
145174

146175
// Default: Return all data (backward compatible)
176+
// Check abort before expensive operations
177+
throwIfAborted(request)
178+
147179
const [timelineData, topSubdomains, topPaths] = await Promise.all([
148-
getTimelineData(whereClause, params, timelineGranularity || 'auto').catch((e) => {
149-
console.error("❌ Timeline Error:", e)
180+
getTimelineData(whereClause, params, timelineGranularity || 'auto', signal).catch((e) => {
181+
if (e instanceof Error && e.name !== 'AbortError') {
182+
console.error("❌ Timeline Error:", e)
183+
}
150184
return []
151185
}),
152-
getTopSubdomains(whereClause, params, 10, searchType, body.keywordMode || 'full-url', targetDomain).catch((e) => {
153-
console.error("❌ Subdomains Error:", e)
186+
getTopSubdomains(whereClause, params, 10, searchType, body.keywordMode || 'full-url', targetDomain, signal).catch((e) => {
187+
if (e instanceof Error && e.name !== 'AbortError') {
188+
console.error("❌ Subdomains Error:", e)
189+
}
154190
return []
155191
}),
156-
getTopPaths(whereClause, params, 10).catch((e) => {
157-
console.error("❌ Paths Error:", e)
192+
getTopPaths(whereClause, params, 10, signal).catch((e) => {
193+
if (e instanceof Error && e.name !== 'AbortError') {
194+
console.error("❌ Paths Error:", e)
195+
}
158196
return []
159197
}),
160198
])
199+
200+
// Check abort after operations
201+
throwIfAborted(request)
161202

162203
console.log("✅ Overview data retrieved:", {
163204
timelineCount: timelineData?.length || 0,
@@ -176,12 +217,19 @@ export async function POST(request: NextRequest) {
176217
})
177218

178219
} catch (error) {
220+
// Handle abort errors gracefully
221+
const abortResponse = handleAbortError(error)
222+
if (abortResponse) {
223+
return abortResponse
224+
}
225+
226+
// Handle other errors
179227
console.error("❌ Error in overview API:", error)
180228
return NextResponse.json({ error: "Failed to get overview data" }, { status: 500 })
181229
}
182230
}
183231

184-
async function getTimelineData(whereClause: string, params: Record<string, string>, granularity: string) {
232+
async function getTimelineData(whereClause: string, params: Record<string, string>, granularity: string, signal?: AbortSignal) {
185233
// OPTIMIZED DATE PARSING STRATEGY (POST-NORMALIZATION)
186234
// After normalization, log_date is already in standard YYYY-MM-DD format
187235
// Query becomes very simple and fast - directly toDate() without complex parsing
@@ -209,7 +257,8 @@ async function getTimelineData(whereClause: string, params: Record<string, strin
209257
LEFT JOIN devices d ON c.device_id = d.device_id
210258
LEFT JOIN systeminformation si ON d.device_id = si.device_id
211259
${whereClause}`,
212-
params
260+
params,
261+
signal
213262
)) as any[]
214263

215264
const range = dateRangeResult[0]
@@ -254,7 +303,7 @@ async function getTimelineData(whereClause: string, params: Record<string, strin
254303
}
255304

256305
console.log("📅 Executing timeline query with granularity:", actualGranularity)
257-
const result = (await executeClickHouseQuery(query, params)) as any[]
306+
const result = (await executeClickHouseQuery(query, params, signal)) as any[]
258307

259308
console.log("📊 Timeline query result:", result.length, "entries")
260309

@@ -270,7 +319,8 @@ async function getTopSubdomains(
270319
limit: number,
271320
searchType: string,
272321
keywordMode: string,
273-
keyword: string
322+
keyword: string,
323+
signal?: AbortSignal
274324
) {
275325
// SECURITY: Validate limit parameter
276326
const safeLimit = Math.min(1000, Math.max(1, Math.floor(Number(limit)) || 10))
@@ -296,7 +346,7 @@ async function getTopSubdomains(
296346
FROM credentials c ${whereClause} GROUP BY full_hostname ORDER BY credential_count DESC LIMIT {queryLimit:UInt32}`
297347
}
298348

299-
const result = (await executeClickHouseQuery(query, queryParams)) as any[]
349+
const result = (await executeClickHouseQuery(query, queryParams, signal)) as any[]
300350

301351
// IMPORTANT: Cast count() to Number (ClickHouse returns String)
302352
return result.map((row: any) => ({
@@ -305,7 +355,7 @@ async function getTopSubdomains(
305355
}))
306356
}
307357

308-
async function getTopPaths(whereClause: string, params: Record<string, string>, limit: number) {
358+
async function getTopPaths(whereClause: string, params: Record<string, string>, limit: number, signal?: AbortSignal) {
309359
// SECURITY: Validate limit parameter
310360
const safeLimit = Math.min(1000, Math.max(1, Math.floor(Number(limit)) || 10))
311361

@@ -320,7 +370,8 @@ async function getTopPaths(whereClause: string, params: Record<string, string>,
320370
GROUP BY path
321371
ORDER BY credential_count DESC
322372
LIMIT {queryLimit:UInt32}`,
323-
{ ...params, queryLimit: safeLimit }
373+
{ ...params, queryLimit: safeLimit },
374+
signal
324375
)) as any[]
325376

326377
// IMPORTANT: Cast count() to Number (ClickHouse returns String)

app/api/domain-recon/passwords/route.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextRequest, NextResponse } from "next/server"
22
import { executeQuery as executeClickHouseQuery } from "@/lib/clickhouse"
33
import { validateRequest } from "@/lib/auth"
4+
import { throwIfAborted, getRequestSignal, handleAbortError } from "@/lib/api-helpers"
45

56
/**
67
* Build WHERE clause for domain matching that supports subdomains (ClickHouse version)
@@ -58,19 +59,28 @@ function buildKeywordWhereClause(keyword: string, mode: 'domain-only' | 'full-ur
5859
}
5960

6061
export async function POST(request: NextRequest) {
62+
// ✅ Check abort VERY EARLY - before validateRequest
63+
throwIfAborted(request)
64+
6165
const user = await validateRequest(request)
6266
if (!user) {
6367
return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 })
6468
}
6569

6670
try {
71+
// Check if request was aborted early
72+
throwIfAborted(request)
73+
6774
const body = await request.json()
6875
const { targetDomain, searchType = 'domain', keywordMode } = body
6976

7077
if (!targetDomain || typeof targetDomain !== 'string') {
7178
return NextResponse.json({ error: "targetDomain is required" }, { status: 400 })
7279
}
7380

81+
// Get signal for passing to database queries
82+
const signal = getRequestSignal(request)
83+
7484
let whereClause: string
7585
let params: Record<string, string>
7686

@@ -92,6 +102,9 @@ export async function POST(request: NextRequest) {
92102
params = result.params
93103
}
94104

105+
// Check abort before expensive operations
106+
throwIfAborted(request)
107+
95108
console.log("🔑 Getting top passwords (optimized query)...")
96109

97110
// OPTIMIZED QUERY (ClickHouse):
@@ -113,8 +126,12 @@ export async function POST(request: NextRequest) {
113126
GROUP BY c.password
114127
ORDER BY total_count DESC, c.password ASC
115128
LIMIT 10`,
116-
params
129+
params,
130+
signal
117131
)) as any[]
132+
133+
// Check abort after operations
134+
throwIfAborted(request)
118135

119136
console.log("🔑 Top passwords query result:", result.length, "items")
120137

@@ -128,6 +145,13 @@ export async function POST(request: NextRequest) {
128145
topPasswords: topPasswords || [],
129146
})
130147
} catch (error) {
148+
// Handle abort errors gracefully
149+
const abortResponse = handleAbortError(error)
150+
if (abortResponse) {
151+
return abortResponse
152+
}
153+
154+
// Handle other errors
131155
console.error("❌ Error in passwords API:", error)
132156
return NextResponse.json(
133157
{

0 commit comments

Comments
 (0)