mirror of
https://github.com/reconurge/flowsint.git
synced 2026-04-28 10:22:58 -05:00
feat: scanner
This commit is contained in:
@@ -8,20 +8,25 @@ app = FastAPI()
|
||||
|
||||
class EmailScanItem(BaseModel):
|
||||
email: str
|
||||
investigation_id:str
|
||||
|
||||
@app.post("/scan/")
|
||||
async def scan(item: EmailScanItem, db: Client = Depends(get_db)):
|
||||
task_name = "email_scan"
|
||||
task = celery_app.send_task(task_name, args=[item.email])
|
||||
assert item.email is not None
|
||||
assert item.investigation_id is not None
|
||||
try:
|
||||
response = db.table("scans").insert({
|
||||
"id": task.id,
|
||||
"status": "pending",
|
||||
"scan_name": task_name
|
||||
"scan_name": task_name,
|
||||
"value":item.email,
|
||||
"investigation_id": item.investigation_id
|
||||
}).execute()
|
||||
return {"id": task.id, "response": response}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Server error: {str(e)}")
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createClient } from "@/lib/supabase/server"
|
||||
import { NextResponse } from "next/server"
|
||||
|
||||
export async function GET(_: Request, { params }: { params: Promise<{ investigation_id: string, scan_id: string }> }) {
|
||||
const { investigation_id, scan_id } = await params
|
||||
try {
|
||||
const supabase = await createClient()
|
||||
const {
|
||||
data: { user },
|
||||
error: userError,
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user || userError) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
const { data: scan, error } = await supabase
|
||||
.from('scans')
|
||||
.select(`*`)
|
||||
.eq("investigation_id", investigation_id)
|
||||
.eq('id', scan_id)
|
||||
.single()
|
||||
if (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
return NextResponse.json(scan)
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { createClient } from "@/lib/supabase/server"
|
||||
import { NextResponse } from "next/server"
|
||||
|
||||
export async function GET(_: Request, { params }: { params: Promise<{ investigation_id: string }> }) {
|
||||
const { investigation_id } = await params
|
||||
try {
|
||||
const supabase = await createClient()
|
||||
const {
|
||||
data: { user },
|
||||
error: userError,
|
||||
} = await supabase.auth.getUser();
|
||||
if (!user || userError) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
const { data: scans, error } = await supabase
|
||||
.from('scans')
|
||||
.select(`*`)
|
||||
.eq("investigation_id", investigation_id)
|
||||
.order("created_at", { ascending: false })
|
||||
if (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
return NextResponse.json(scans)
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ interface DashboardClientProps {
|
||||
}
|
||||
export default function DashboardClient({ investigationId }: DashboardClientProps) {
|
||||
// Use the initial data from the server, but enable background updates
|
||||
const [view, setView] = useQueryState("view", { defaultValue: "flow-graph" })
|
||||
const [view, _] = useQueryState("view", { defaultValue: "flow-graph" })
|
||||
const graphQuery = useQuery({
|
||||
queryKey: ["investigation", investigationId, "data"],
|
||||
queryFn: async () => {
|
||||
|
||||
@@ -83,7 +83,7 @@ function EmailNode({ data }: any) {
|
||||
<ContextMenuSubTrigger>Search</ContextMenuSubTrigger>
|
||||
<ContextMenuSubContent>
|
||||
<ContextMenuItem onClick={() => {
|
||||
toast.promise(checkEmail(data.email), {
|
||||
toast.promise(checkEmail(data.email, data.investigation_id), {
|
||||
loading: 'Loading...',
|
||||
success: () => {
|
||||
return `Scan on ${data.email} has been launched.`;
|
||||
|
||||
@@ -22,6 +22,9 @@ import { Input } from "@/components/ui/input"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
import { useQueryState } from "nuqs"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useParams } from "next/navigation"
|
||||
import Loader from "../loader"
|
||||
|
||||
export type ScanResult = {
|
||||
name: string
|
||||
@@ -41,6 +44,7 @@ export type ErrorResult = {
|
||||
|
||||
export type Scan = {
|
||||
id: string
|
||||
value: string,
|
||||
status: "pending" | "finished" | "error"
|
||||
results: {
|
||||
results: (ScanResult | ErrorResult)[]
|
||||
@@ -48,14 +52,26 @@ export type Scan = {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
|
||||
export function ScanDrawer() {
|
||||
const [currentScan, setCurrentScan] = useState<Scan | null>(null)
|
||||
const { investigation_id } = useParams()
|
||||
const [scanId, setScanId] = useQueryState("scan_id")
|
||||
const [open, setOpen] = useState(Boolean(scanId))
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [viewMode, setViewMode] = useState<"grid" | "table">("table")
|
||||
|
||||
const { data: currentScan = null, isLoading } = useQuery({
|
||||
queryKey: ["investigation", investigation_id, "scans", scanId],
|
||||
queryFn: async (): Promise<Scan | null> => {
|
||||
const res = await fetch(`/api/investigations/${investigation_id}/scans/${scanId}`)
|
||||
if (!res.ok) {
|
||||
return null
|
||||
}
|
||||
return res.json()
|
||||
},
|
||||
enabled: !!scanId,
|
||||
refetchOnWindowFocus: true,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (Boolean(scanId)) setOpen(true)
|
||||
}, [scanId])
|
||||
@@ -64,22 +80,11 @@ export function ScanDrawer() {
|
||||
if (!open) setScanId(null)
|
||||
}, [open])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const fetchScan = async () => {
|
||||
if (scanId) {
|
||||
const { data: scan } = await supabase.from("scans").select("*").eq("id", scanId).single()
|
||||
setCurrentScan(scan)
|
||||
}
|
||||
}
|
||||
fetchScan()
|
||||
}, [supabase, setCurrentScan, scanId])
|
||||
|
||||
const isErrorResult = (result: ScanResult | ErrorResult): result is ErrorResult => {
|
||||
return "error" in result
|
||||
}
|
||||
|
||||
const filteredResults = currentScan?.results.results.filter((result: any) => {
|
||||
const filteredResults = currentScan?.results ? currentScan?.results?.results.filter((result: any) => {
|
||||
if (isErrorResult(result)) {
|
||||
return result.error.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
}
|
||||
@@ -87,9 +92,9 @@ export function ScanDrawer() {
|
||||
result.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
result.domain.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
})
|
||||
}) : null
|
||||
|
||||
const stats = currentScan?.results.results.reduce(
|
||||
const stats = currentScan?.results ? currentScan?.results.results.reduce(
|
||||
(acc: any, result: any) => {
|
||||
if (isErrorResult(result)) {
|
||||
acc.errors++
|
||||
@@ -100,195 +105,216 @@ export function ScanDrawer() {
|
||||
return acc
|
||||
},
|
||||
{ exists: 0, rateLimited: 0, errors: 0 },
|
||||
)
|
||||
) : null
|
||||
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<DrawerContent className="h-[95vh] !max-h-none">
|
||||
<DrawerHeader className="border-b pb-4 p-4">
|
||||
<DrawerTitle className="flex items-center gap-2 mb-2">
|
||||
Scan Results
|
||||
<Badge variant={currentScan?.status === "error" ? "destructive" : "outline"}>{currentScan?.status}</Badge>
|
||||
</DrawerTitle>
|
||||
<p className="opacity-60 mb-2 mt-2">{currentScan?.id}</p>
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-4">
|
||||
<Card className="bg-background shadow-none">
|
||||
<CardContent className="p-4 flex items-center gap-2">
|
||||
<Shield className="h-4 w-4 text-green-500" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Existing Accounts</p>
|
||||
<p className="text-2xl font-bold">{stats?.exists || 0}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-background shadow-none">
|
||||
<CardContent className="p-4 flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-yellow-500" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Rate Limited</p>
|
||||
<p className="text-2xl font-bold">{stats?.rateLimited || 0}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-background shadow-none">
|
||||
<CardContent className="p-4 flex items-center gap-2">
|
||||
<ShieldAlert className="h-4 w-4 text-destructive" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Errors</p>
|
||||
<p className="text-2xl font-bold">{stats?.errors || 0}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{/* Search and View Toggle */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search services..."
|
||||
className="pl-8 shadow-none"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={() => setViewMode(viewMode === "grid" ? "table" : "grid")}>
|
||||
{viewMode === "grid" ? <LayoutList className="h-4 w-4" /> : <Grid2X2 className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</DrawerHeader>
|
||||
<div className="h-auto overflow-auto">
|
||||
{viewMode === "table" ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Service</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Method</TableHead>
|
||||
<TableHead>Details</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredResults?.map((result: any, index: number) => (
|
||||
<TableRow key={index}>
|
||||
{isErrorResult(result) ? (
|
||||
<>
|
||||
<TableCell className="font-medium dark:text-red-400 text-destructive px-4"><div>Error</div></TableCell>
|
||||
<TableCell colSpan={3} className="dark:text-red-400 text-destructive">
|
||||
{result.error}
|
||||
</TableCell>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
{result.name}
|
||||
<a
|
||||
href={`https://${result.domain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground hover:text-primary"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{result.exists ? (
|
||||
<Badge className="text-green-500 bg-green-400/10 border border-green-500/30">
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
Exists
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">
|
||||
<XCircle className="h-3 w-3 mr-1" />
|
||||
Not Found
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{result.method}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
{result.rateLimit && (
|
||||
<Badge variant="destructive" className="text-yellow-500 bg-yellow-400/10 border border-yellow-500/30">
|
||||
Rate Limited
|
||||
</Badge>
|
||||
)}
|
||||
{result.frequent_rate_limit && (
|
||||
<Badge variant="destructive" className="text-orange-500 bg-orange-400/10 border border-orange-500/30">
|
||||
Frequent Limits
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 p-4 overflow-auto">
|
||||
{filteredResults?.map((result: any, index: number) => (
|
||||
<Card key={index} className="overflow-hidden bg-background">
|
||||
<CardContent className="p-4">
|
||||
{isErrorResult(result) ? (
|
||||
<div className="flex items-start gap-2 text-destructive">
|
||||
<AlertCircle className="h-4 w-4 mt-1" />
|
||||
<div>
|
||||
<p className="font-medium">Error</p>
|
||||
<p className="text-sm">{result.error}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
{result.name}
|
||||
<a
|
||||
href={`https://${result.domain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground hover:text-primary"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
{result.exists ? (
|
||||
<Badge className="bg-green-500">
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
Exists
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">
|
||||
<XCircle className="h-3 w-3 mr-1" />
|
||||
Not Found
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Method: {result.method}</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{result.rateLimit && (
|
||||
<Badge variant="destructive" className="bg-yellow-500">
|
||||
Rate Limited
|
||||
</Badge>
|
||||
)}
|
||||
{result.frequent_rate_limit && (
|
||||
<Badge variant="destructive" className="bg-orange-500">
|
||||
Frequent Limits
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DrawerContent className="h-[95vh] !max-h-none border">
|
||||
{isLoading &&
|
||||
<DrawerHeader className="border-b pb-4 p-4">
|
||||
<DrawerTitle className="flex items-center gap-2 mb-2">
|
||||
Scan Results
|
||||
<Badge variant={currentScan?.status === "error" ? "destructive" : "outline"}>{currentScan?.status}</Badge>
|
||||
</DrawerTitle>
|
||||
<p className="opacity-60 mb-2 mt-2">{currentScan?.value}</p>
|
||||
<div className="h-[400px] w-full flex items-center justify-center gap-1"><Loader /> Loading results...</div>
|
||||
</DrawerHeader>}
|
||||
{!isLoading && stats && filteredResults ?
|
||||
<>
|
||||
<DrawerHeader className="border-b pb-4 p-4">
|
||||
<DrawerTitle className="flex items-center gap-2 mb-2">
|
||||
Scan Results
|
||||
<Badge variant={currentScan?.status === "error" ? "destructive" : "outline"}>{currentScan?.status}</Badge>
|
||||
</DrawerTitle>
|
||||
<p className="opacity-60 mb-2 mt-2">{currentScan?.value}</p>
|
||||
{/* Stats Cards */}
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 mb-4">
|
||||
<Card className="bg-background shadow-none">
|
||||
<CardContent className="p-4 flex items-center gap-2">
|
||||
<Shield className="h-4 w-4 text-green-500" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Existing Accounts</p>
|
||||
<p className="text-2xl font-bold">{stats?.exists || 0}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
<Card className="bg-background shadow-none">
|
||||
<CardContent className="p-4 flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-yellow-500" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Rate Limited</p>
|
||||
<p className="text-2xl font-bold">{stats?.rateLimited || 0}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-background shadow-none">
|
||||
<CardContent className="p-4 flex items-center gap-2">
|
||||
<ShieldAlert className="h-4 w-4 text-destructive" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Errors</p>
|
||||
<p className="text-2xl font-bold">{stats?.errors || 0}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{/* Search and View Toggle */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search services..."
|
||||
className="pl-8 shadow-none"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon" onClick={() => setViewMode(viewMode === "grid" ? "table" : "grid")}>
|
||||
{viewMode === "grid" ? <LayoutList className="h-4 w-4" /> : <Grid2X2 className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
</DrawerHeader>
|
||||
<div className="h-auto overflow-auto">
|
||||
{viewMode === "table" ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Service</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Method</TableHead>
|
||||
<TableHead>Details</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredResults?.map((result: any, index: number) => (
|
||||
<TableRow key={index}>
|
||||
{isErrorResult(result) ? (
|
||||
<>
|
||||
<TableCell className="font-medium dark:text-red-400 text-destructive px-4"><div>Error</div></TableCell>
|
||||
<TableCell colSpan={3} className="dark:text-red-400 text-destructive">
|
||||
{result.error}
|
||||
</TableCell>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2 px-2">
|
||||
{result.name}
|
||||
<a
|
||||
href={`https://${result.domain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground hover:text-primary"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
{result.exists ? (
|
||||
<Badge className="text-green-500 bg-green-400/10 border border-green-500/30">
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
Exists
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">
|
||||
<XCircle className="h-3 w-3 mr-1" />
|
||||
Not Found
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{result.method}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
{result.rateLimit && (
|
||||
<Badge variant="destructive" className="text-yellow-500 bg-yellow-400/10 border border-yellow-500/30">
|
||||
Rate Limited
|
||||
</Badge>
|
||||
)}
|
||||
{result.frequent_rate_limit && (
|
||||
<Badge variant="destructive" className="text-orange-500 bg-orange-400/10 border border-orange-500/30">
|
||||
Frequent Limits
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 p-4 overflow-auto">
|
||||
{filteredResults?.map((result: any, index: number) => (
|
||||
<Card key={index} className="overflow-hidden bg-background">
|
||||
<CardContent className="p-4">
|
||||
{isErrorResult(result) ? (
|
||||
<div className="flex items-start gap-2 text-destructive">
|
||||
<AlertCircle className="h-4 w-4 mt-1" />
|
||||
<div>
|
||||
<p className="font-medium">Error</p>
|
||||
<p className="text-sm">{result.error}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
{result.name}
|
||||
<a
|
||||
href={`https://${result.domain}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-muted-foreground hover:text-primary"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</div>
|
||||
{result.exists ? (
|
||||
<Badge className="bg-green-500">
|
||||
<CheckCircle2 className="h-3 w-3 mr-1" />
|
||||
Exists
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">
|
||||
<XCircle className="h-3 w-3 mr-1" />
|
||||
Not Found
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Method: {result.method}</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{result.rateLimit && (
|
||||
<Badge variant="destructive" className="bg-yellow-500">
|
||||
Rate Limited
|
||||
</Badge>
|
||||
)}
|
||||
{result.frequent_rate_limit && (
|
||||
<Badge variant="destructive" className="bg-orange-500">
|
||||
Frequent Limits
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</> : !isLoading && <div><DrawerHeader className="border-b pb-4 p-4">
|
||||
<DrawerTitle className="flex items-center gap-2 mb-2">
|
||||
Scan Results
|
||||
<Badge variant={currentScan?.status === "error" ? "destructive" : "outline"}>{currentScan?.status}</Badge>
|
||||
</DrawerTitle>
|
||||
<p className="opacity-60 mb-2 mt-2">{currentScan?.value}</p>
|
||||
<div>Results are not there yet. They should appear soon.</div>
|
||||
</DrawerHeader>
|
||||
</div>}
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Zap } from "lucide-react"
|
||||
import { Zap, ZapIcon } from "lucide-react"
|
||||
import { createClient } from "@supabase/supabase-js"
|
||||
import { useQueryState } from "nuqs"
|
||||
|
||||
@@ -27,7 +27,7 @@ const supabase = createClient(supabaseUrl, supabaseKey)
|
||||
|
||||
export type Scan = {
|
||||
id: string
|
||||
scan_name: string
|
||||
value: string
|
||||
status: "pending" | "finished" | "failed"
|
||||
results: { results: any[] }
|
||||
}
|
||||
@@ -72,11 +72,12 @@ export function ScanButton() {
|
||||
setScans((current) => {
|
||||
const newScans = current.map((scan) => (scan.id === payload.new.id ? (payload.new as Scan) : scan))
|
||||
updatePendingCount(newScans)
|
||||
toast('Scan complete ! view results.', {
|
||||
action: {
|
||||
label: 'Results',
|
||||
onClick: () => setScanId(payload.new.id),
|
||||
}
|
||||
toast(<div>
|
||||
<p className="text-primary font-medium">{payload.new.value}</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<p> Scan complete on this item.</p> <Button variant="outline" onClick={() => setScanId(payload.new.id)}>Click to see results</Button>
|
||||
</div></div>, {
|
||||
duration: 6000,
|
||||
})
|
||||
return newScans
|
||||
})
|
||||
@@ -110,20 +111,25 @@ export function ScanButton() {
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size={"icon"} className="h-full relative w-12 rounded-none">
|
||||
<Zap className="mr-2 h-4 w-4" />
|
||||
{pendingCount > 0 ? (
|
||||
<Badge variant="default" className="ml-2 bg-primary/50 absolute top-1 right-1 text-primary-foreground rounded-full">
|
||||
{pendingCount}
|
||||
</Badge>
|
||||
<>
|
||||
<ZapIcon className="mr-2 !h-4 !w-4" />
|
||||
<Badge variant="default" className="text-xs px-0 h-5 w-5 absolute top-1 right-1 border-none rounded-full">
|
||||
{pendingCount}
|
||||
</Badge>
|
||||
</>
|
||||
) : (
|
||||
<Badge variant="outline" className="ml-2 absolute top-1 right-1 border-none rounded-full">
|
||||
0
|
||||
</Badge>
|
||||
<>
|
||||
<ZapIcon className="mr-2 !h-4 !w-4" />
|
||||
<Badge variant="outline" className="text-xs px-0 h-5 w-5 absolute top-1 right-1 border-none rounded-full">
|
||||
0
|
||||
</Badge>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<div className="mx-auto w-full max-w-4xl h-screen overflow-auto">
|
||||
<SheetContent className="w-full !max-w-[500px]">
|
||||
<div className="mx-auto w-full !max-w-[500px] h-screen overflow-auto">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Scans</SheetTitle>
|
||||
<SheetDescription>View all your scans. Click on a scan to select it.</SheetDescription>
|
||||
|
||||
@@ -15,7 +15,7 @@ export function ScanTable({ scans, onScanClick, selectedScanId }: ScanTableProps
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Scan Name</TableHead>
|
||||
<TableHead>Item</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Results</TableHead>
|
||||
</TableRow>
|
||||
@@ -34,7 +34,7 @@ export function ScanTable({ scans, onScanClick, selectedScanId }: ScanTableProps
|
||||
className={`cursor-pointer hover:bg-muted/50 ${selectedScanId === scan.id ? "bg-muted" : ""}`}
|
||||
onClick={() => onScanClick(scan.id)}
|
||||
>
|
||||
<TableCell className="font-medium">{scan.scan_name}</TableCell>
|
||||
<TableCell className="font-medium"><span className=" max-w-[200px] block truncate text-ellipsis">{scan.value}</span></TableCell>
|
||||
<TableCell>
|
||||
{scan.status === "pending" ? (
|
||||
<Badge variant="default" className="bg-yellow-500 hover:bg-yellow-600">
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
'use server'
|
||||
import { createClient } from "../supabase/server";
|
||||
|
||||
export async function checkEmail(email: string) {
|
||||
export async function checkEmail(email: string, investigation_id: string) {
|
||||
const url = `http://localhost:5000/scan/`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"email": email
|
||||
})
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
);
|
||||
return response.json();
|
||||
body: JSON.stringify({
|
||||
"email": email,
|
||||
"investigation_id": investigation_id
|
||||
})
|
||||
},
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function checkBreachedAccount(account: string | number | boolean, apiKey: string, appName: string) {
|
||||
|
||||
Reference in New Issue
Block a user