From e60cf02540b7f6a6a65f6c490405faae505daaef Mon Sep 17 00:00:00 2001 From: dextmorgn Date: Fri, 21 Feb 2025 14:57:16 +0100 Subject: [PATCH] feat: move-to-api-for-fetching --- next.config.js | 3 + package.json | 1 + src/app/api/chat/route.ts | 12 +- .../[investigation_id]/data/route.ts | 144 +++++++++ .../[investigation_id]/emails/route.ts | 29 ++ .../[investigation_id]/individuals/route.ts | 29 ++ .../[investigation_id]/ips/route.ts | 27 ++ .../[investigation_id]/phones/route.ts | 27 ++ .../[investigation_id]/route.ts | 31 ++ src/app/api/investigations/route.ts | 23 ++ src/app/dashboard/page.tsx | 8 +- .../[investigation_id]/client.tsx | 29 ++ .../[investigation_id]/layout.tsx | 5 +- .../[investigation_id]/left.tsx | 212 +++++++------ .../[investigation_id]/page.tsx | 27 +- src/app/providers.tsx | 18 +- .../contexts/investigation-provider.tsx | 108 +++---- src/components/contexts/search-context.tsx | 2 +- .../investigations/case-selector.tsx | 15 +- src/components/investigations/graph.tsx | 21 +- .../investigations/individual-modal.tsx | 1 - src/components/investigations/nodes/email.tsx | 2 +- .../investigations/nodes/ip_address.tsx | 2 +- src/components/investigations/nodes/phone.tsx | 2 +- .../investigations/nodes/physical_address.tsx | 2 +- .../investigations/nodes/social.tsx | 2 +- src/lib/actions/investigations.ts | 171 +--------- src/lib/actions/search.ts | 7 +- src/lib/supabase/middleware.ts | 11 +- src/lib/supabase/server.ts | 14 + src/middleware.ts | 18 ++ src/store/flow-store.ts | 15 - src/store/investigation-store.ts | 292 +++++++++--------- src/types/investigation.ts | 40 ++- yarn.lock | 12 + 35 files changed, 818 insertions(+), 544 deletions(-) create mode 100644 src/app/api/investigations/[investigation_id]/data/route.ts create mode 100644 src/app/api/investigations/[investigation_id]/emails/route.ts create mode 100644 src/app/api/investigations/[investigation_id]/individuals/route.ts create mode 100644 src/app/api/investigations/[investigation_id]/ips/route.ts create mode 100644 src/app/api/investigations/[investigation_id]/phones/route.ts create mode 100644 src/app/api/investigations/[investigation_id]/route.ts create mode 100644 src/app/api/investigations/route.ts create mode 100644 src/app/investigations/[investigation_id]/client.tsx create mode 100644 src/middleware.ts diff --git a/next.config.js b/next.config.js index eebdb22..2dd01c7 100644 --- a/next.config.js +++ b/next.config.js @@ -1,5 +1,8 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + experimental: { + authInterrupts: true, + }, images: { remotePatterns: [ { diff --git a/package.json b/package.json index fedab9e..4d3d56b 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@supabase/supabase-js": "^2.48.1", "@tailwindcss/cli": "^4.0.3", "@tailwindcss/postcss": "^4.0.3", + "@tanstack/react-query": "^5.66.8", "@xyflow/react": "^12.4.2", "ai": "^4.1.34", "class-variance-authority": "^0.7.1", diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 1b8ca12..a4fcd73 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,10 +1,20 @@ import { mistral } from '@ai-sdk/mistral'; import { streamText } from 'ai'; - +import { createClient } from "@/lib/supabase/server" +import { NextResponse } from "next/server" // Allow streaming responses up to 30 seconds export const maxDuration = 30; export async function POST(req: Request) { + const supabase = await createClient() + + const { + data: { user }, + error: userError, + } = await supabase.auth.getUser(); + if (!user || userError) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + } const { messages } = await req.json(); const result = streamText({ diff --git a/src/app/api/investigations/[investigation_id]/data/route.ts b/src/app/api/investigations/[investigation_id]/data/route.ts new file mode 100644 index 0000000..a4f0c7b --- /dev/null +++ b/src/app/api/investigations/[investigation_id]/data/route.ts @@ -0,0 +1,144 @@ +import { createClient } from "@/lib/supabase/server" +import type { NodeData, EdgeData } from "@/types" +import { NextResponse } from "next/server" + +export async function GET(_: Request, { params }: { params: Promise<{ investigation_id: string }> }) { + const { investigation_id } = await params + const supabase = await createClient() + try { + const { + data: { user }, + error: userError, + } = await supabase.auth.getUser(); + if (!user || userError) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) + } const { data: individuals, error: indError } = await supabase + .from("individuals") + .select("*, ip_addresses(*), phone_numbers(*), social_accounts(*), emails(*), physical_addresses(*)") + .eq("investigation_id", investigation_id) + if (indError) { + return NextResponse.json({ error: indError.message }, { status: 500 }) + } + if (!individuals || individuals.length === 0) { + return NextResponse.json({ nodes: [], edges: [] }) + } + // Extraire les IDs des individus + const individualIds = individuals.map((ind) => ind.id) + // Récupérer les relations + const { data: relations, error: relError } = await supabase + .from("relationships") + .select("individual_a, individual_b, relation_type, confidence_level") + .in("individual_a", individualIds) + .in("individual_b", individualIds) + if (relError) { + return NextResponse.json({ error: relError.message }, { status: 500 }) + } + const nodes: NodeData[] = [] + const edges: EdgeData[] = [] + // Construire les nœuds et les arêtes + individuals.forEach((ind: any) => { + const individualId = ind.id.toString() + nodes.push({ + id: individualId, + type: "individual", + data: { ...ind, label: ind.full_name }, + position: { x: 0, y: 100 }, + }) + // Ajouter les emails + ind.emails?.forEach((email: any) => { + nodes.push({ + id: email.id.toString(), + type: "email", + data: { ...email, label: email.email }, + position: { x: 100, y: 100 }, + }) + edges.push({ + source: individualId, + target: email.id.toString(), + type: "custom", + id: `${individualId}-${email.id}`.toString(), + label: "email", + }) + }) + // Ajouter les numéros de téléphone + ind.phone_numbers?.forEach((phone: any) => { + nodes.push({ + id: phone.id.toString(), + type: "phone", + data: { ...phone, label: phone.phone_number }, + position: { x: -100, y: 100 }, + }) + edges.push({ + source: individualId, + target: phone.id.toString(), + type: "custom", + id: `${individualId}-${phone.id}`.toString(), + label: "phone", + }) + }) + // Ajouter les comptes sociaux + ind.social_accounts?.forEach((social: any) => { + nodes.push({ + id: social.id.toString(), + type: "social", + data: { ...social, label: `${social.platform}: ${social.username}` }, + position: { x: 100, y: -100 }, + }) + edges.push({ + source: individualId, + target: social.id.toString(), + type: "custom", + id: `${individualId}-${social.id}`.toString(), + label: "social", + }) + }) + // Ajouter les adresses IP + ind.ip_addresses?.forEach((ip: any) => { + nodes.push({ + id: ip.id.toString(), + type: "ip", + data: { label: ip.ip_address }, + position: { x: -100, y: -100 }, + }) + edges.push({ + source: individualId, + target: ip.id.toString(), + type: "custom", + id: `${individualId}-${ip.id}`.toString(), + label: "IP", + }) + }) + // Ajouter les adresses physiques + ind.physical_addresses?.forEach((address: any) => { + nodes.push({ + id: address.id.toString(), + type: "address", + data: { ...address, label: [address.address, address.city, address.country].join(", ") }, + position: { x: 100, y: 100 }, + }) + edges.push({ + source: individualId, + target: address.id.toString(), + type: "custom", + id: `${individualId}-${address.id}`.toString(), + label: "address", + }) + }) + }) + // Ajouter les relations entre individus + relations?.forEach(({ individual_a, individual_b, relation_type, confidence_level }) => { + edges.push({ + source: individual_a.toString(), + target: individual_b.toString(), + type: "custom", + id: `${individual_a}-${individual_b}`.toString(), + label: relation_type, + confidence_level, + }) + }) + return NextResponse.json({ nodes, edges }) + } catch (error) { + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }) + } +} + diff --git a/src/app/api/investigations/[investigation_id]/emails/route.ts b/src/app/api/investigations/[investigation_id]/emails/route.ts new file mode 100644 index 0000000..b03739c --- /dev/null +++ b/src/app/api/investigations/[investigation_id]/emails/route.ts @@ -0,0 +1,29 @@ +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: emails, error } = await supabase + .from('emails') + .select(` + * + `) + .eq("investigation_id", investigation_id) + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + return NextResponse.json({ emails }) + } catch (error) { + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }) + } +} \ No newline at end of file diff --git a/src/app/api/investigations/[investigation_id]/individuals/route.ts b/src/app/api/investigations/[investigation_id]/individuals/route.ts new file mode 100644 index 0000000..8d738ed --- /dev/null +++ b/src/app/api/investigations/[investigation_id]/individuals/route.ts @@ -0,0 +1,29 @@ +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: individuals, error } = await supabase + .from('individuals') + .select(` + * + `) + .eq("investigation_id", investigation_id) + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + return NextResponse.json({ individuals }) + } catch (error) { + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }) + } +} \ No newline at end of file diff --git a/src/app/api/investigations/[investigation_id]/ips/route.ts b/src/app/api/investigations/[investigation_id]/ips/route.ts new file mode 100644 index 0000000..ee46009 --- /dev/null +++ b/src/app/api/investigations/[investigation_id]/ips/route.ts @@ -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: ips, error } = await supabase + .from('ip_addresses') + .select(`*`) + .eq("investigation_id", investigation_id) + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + return NextResponse.json({ ips }) + } catch (error) { + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }) + } +} \ No newline at end of file diff --git a/src/app/api/investigations/[investigation_id]/phones/route.ts b/src/app/api/investigations/[investigation_id]/phones/route.ts new file mode 100644 index 0000000..701278c --- /dev/null +++ b/src/app/api/investigations/[investigation_id]/phones/route.ts @@ -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: phones, error } = await supabase + .from('phone_numbers') + .select(`*`) + .eq("investigation_id", investigation_id) + + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + return NextResponse.json({ phones }) + } catch (error) { + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }) + } +} \ No newline at end of file diff --git a/src/app/api/investigations/[investigation_id]/route.ts b/src/app/api/investigations/[investigation_id]/route.ts new file mode 100644 index 0000000..b7f6d77 --- /dev/null +++ b/src/app/api/investigations/[investigation_id]/route.ts @@ -0,0 +1,31 @@ +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: investigation, error } = await supabase + .from("investigations") + .select("id, title, description") + .eq("id", investigation_id) + .single() + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + if (!investigation) { + return NextResponse.json({ error: "Investigation not found" }, { status: 404 }) + } + return NextResponse.json({ investigation }) + } catch (error) { + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }) + } +} + diff --git a/src/app/api/investigations/route.ts b/src/app/api/investigations/route.ts new file mode 100644 index 0000000..d5742e2 --- /dev/null +++ b/src/app/api/investigations/route.ts @@ -0,0 +1,23 @@ +import { createClient } from "@/lib/supabase/server" +import { NextResponse } from "next/server" + +export async function GET() { + 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: investigations, error } = await supabase.from("investigations").select("id, title, description") + if (error) { + return NextResponse.json({ error: error.message }, { status: 500 }) + } + return NextResponse.json({ investigations }) + } catch (error) { + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }) + } +} + diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index d827642..d075ce0 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,13 +1,15 @@ import React from 'react' -import { getInvestigations } from '@/lib/actions/investigations' import Investigation from '@/components/dashboard/investigation' import { Button } from '@/components/ui/button' import { DownloadIcon, FolderIcon, PlusIcon } from 'lucide-react' import NewCase from '@/components/dashboard/new-case' +import { createClient } from '@/lib/supabase/server' +import { unauthorized } from 'next/navigation' const DashboardPage = async () => { - const { investigations, error } = await getInvestigations() - if (error) return
An error occured.
+ const supabase = await createClient() + const { data: investigations, error } = await supabase.from("investigations").select("id, title, description") + if (error) return unauthorized() return (
diff --git a/src/app/investigations/[investigation_id]/client.tsx b/src/app/investigations/[investigation_id]/client.tsx new file mode 100644 index 0000000..41eaf40 --- /dev/null +++ b/src/app/investigations/[investigation_id]/client.tsx @@ -0,0 +1,29 @@ +"use client" +import { useQuery } from "@tanstack/react-query" +import InvestigationGraph from "@/components/investigations/graph" +import IndividualModal from "@/components/investigations/individual-modal" +import { notFound } from "next/navigation" +interface DashboardClientProps { + investigationId: string +} +export default function DashboardClient({ investigationId }: DashboardClientProps) { + // Use the initial data from the server, but enable background updates + const graphQuery = useQuery({ + queryKey: ["investigation", investigationId, "data"], + queryFn: async () => { + const res = await fetch(`/api/investigations/${investigationId}/data`) + if (!res.ok) { + notFound() + } + return res.json() + }, + refetchOnWindowFocus: true, + }) + return ( +
+ + +
+ ) +} + diff --git a/src/app/investigations/[investigation_id]/layout.tsx b/src/app/investigations/[investigation_id]/layout.tsx index 772f124..1ef72ce 100644 --- a/src/app/investigations/[investigation_id]/layout.tsx +++ b/src/app/investigations/[investigation_id]/layout.tsx @@ -3,9 +3,8 @@ import InvestigationLayout from '@/components/investigations/layout'; import Left from './left'; import { SearchProvider } from '@/components/contexts/search-context'; import { createClient } from "@/lib/supabase/server"; -import { notFound, redirect } from "next/navigation"; +import { redirect } from "next/navigation"; import { ChatProvider } from '@/components/contexts/chatbot-context'; -import { getInvestigation } from '@/lib/actions/investigations'; const DashboardLayout = async ({ children, params, @@ -19,8 +18,6 @@ const DashboardLayout = async ({ redirect('/login') } const { investigation_id } = await (params) - const { investigation, error } = await getInvestigation(investigation_id) - if (!investigation || error) return notFound() return ( diff --git a/src/app/investigations/[investigation_id]/left.tsx b/src/app/investigations/[investigation_id]/left.tsx index 3b59329..ea8e274 100644 --- a/src/app/investigations/[investigation_id]/left.tsx +++ b/src/app/investigations/[investigation_id]/left.tsx @@ -3,158 +3,155 @@ import { Skeleton } from "@/components/ui/skeleton" import { cn } from "@/lib/utils" import { AtSignIcon, PhoneIcon, UserIcon } from "lucide-react" import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" -import { useInvestigationStore, useInvestigationData } from "@/store/investigation-store" +import { useInvestigationStore } from "@/store/investigation-store" import { usePlatformIcons } from "@/lib/hooks/use-platform-icons" import { useFlowStore } from "@/store/flow-store" const Left = ({ investigation_id }: { investigation_id: string }) => { const platformsIcons = usePlatformIcons() - useInvestigationData(investigation_id) const { currentNode, setCurrentNode } = useFlowStore() + + // Utiliser le hook useInvestigationData pour récupérer les données const { individuals, - isLoadingIndividuals, emails, - isLoadingEmails, phones, - isLoadingPhones, - socials, - isLoadingSocials, - } = useInvestigationStore() + socials + } = useInvestigationStore((state) => state.useInvestigationData(investigation_id)) + + // Composant réutilisable pour le skeleton loader + const LoadingSkeleton = () => ( +
+ + + +
+ ) return (
- Profiles {!isLoadingIndividuals && <>({individuals?.length})} + Profiles {!individuals.isLoading && <>({individuals.data.length})} - {isLoadingIndividuals && ( -
- - - -
- )} -
    - {individuals?.map((individual: any) => ( -
  • - -
  • - ))} -
+ + + ))} + + )}
+ - Emails {!isLoadingEmails && <>({emails?.length})} + Emails {!emails.isLoading && <>({emails.data.length})} - {isLoadingEmails && ( -
- - - -
+ {emails.isLoading ? ( + + ) : ( +
    + {emails.data.map((email) => ( +
  • + +
  • + ))} +
)} -
    - {emails?.map((email: any) => ( -
  • - -
  • - ))} -
+ - Phones {!isLoadingPhones && <>({phones?.length})} + Phones {!phones.isLoading && <>({phones.data.length})} - {isLoadingPhones && ( -
- - - -
+ {phones.isLoading ? ( + + ) : ( +
    + {phones.data.map((phone) => ( +
  • + +
  • + ))} +
)} -
    - {phones?.map((phone: any) => ( -
  • - -
  • - ))} -
+ - Socials {!isLoadingSocials && <>({socials?.length})} + Socials {!socials.isLoading && <>({socials.data.length})} - {isLoadingSocials && ( -
- - - -
+ {socials.isLoading ? ( + + ) : ( +
    + {socials.data.map((social) => ( +
  • + +
  • + ))} +
)} -
    - {socials?.map((social: any) => ( -
  • - -
  • - ))} -
@@ -162,5 +159,4 @@ const Left = ({ investigation_id }: { investigation_id: string }) => { ) } -export default Left - +export default Left \ No newline at end of file diff --git a/src/app/investigations/[investigation_id]/page.tsx b/src/app/investigations/[investigation_id]/page.tsx index 4bcea2a..2414a07 100644 --- a/src/app/investigations/[investigation_id]/page.tsx +++ b/src/app/investigations/[investigation_id]/page.tsx @@ -1,19 +1,24 @@ -import { getInvestigationData } from '@/lib/actions/investigations' -import InvestigationGraph from '@/components/investigations/graph' -import IndividualModal from '@/components/investigations/individual-modal' +import { notFound, unauthorized } from "next/navigation" +import DashboardClient from "./client" +import { createClient } from "@/lib/supabase/server" +// Server Component for initial data fetch const DashboardPage = async ({ params, }: { params: Promise<{ investigation_id: string }> }) => { + const supabase = await createClient() const { investigation_id } = await (params) - const { nodes, edges } = await getInvestigationData(investigation_id) - return ( -
- - -
- ) + const { + data: { user }, + error: userError, + } = await supabase.auth.getUser(); + if (!user || userError) { + return notFound() + } + return } -export default DashboardPage \ No newline at end of file + +export default DashboardPage + diff --git a/src/app/providers.tsx b/src/app/providers.tsx index fd6117e..30afcd0 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -3,15 +3,29 @@ import type { ThemeProviderProps } from "next-themes"; import * as React from "react"; import { ThemeProvider as NextThemesProvider } from "next-themes"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { useState } from "react" export interface ProvidersProps { children: React.ReactNode; themeProps?: ThemeProviderProps; } - export function Providers({ children, themeProps }: ProvidersProps) { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + // Disable automatic background refetching + staleTime: 60 * 1000, // Consider data stale after 1 minute + }, + }, + }), + ) return ( - {children} + + {children} + ); } diff --git a/src/components/contexts/investigation-provider.tsx b/src/components/contexts/investigation-provider.tsx index 4e07121..9389add 100644 --- a/src/components/contexts/investigation-provider.tsx +++ b/src/components/contexts/investigation-provider.tsx @@ -3,7 +3,6 @@ import type React from "react" import { type ReactNode, useEffect } from "react" import { useInvestigationStore } from "@/store/investigation-store" import { useParams, useRouter, usePathname, useSearchParams } from "next/navigation" -import { useInvestigation } from "@/lib/hooks/investigation/investigation" import { useConfirm } from "@/components/use-confirm-dialog" import { supabase } from "@/lib/supabase/client" import { Dialog, DialogContent, DialogDescription, DialogClose, DialogTitle } from "@/components/ui/dialog" @@ -21,65 +20,49 @@ export const InvestigationProvider: React.FC = ({ ch const pathname = usePathname() const searchParams = useSearchParams() const { confirm } = useConfirm() - const { investigation, isLoading: isLoadingInvestigation } = useInvestigation(investigation_id) - const { settings, setSettings, openSettingsModal, setOpenSettingsModal, - setInvestigation, - setIsLoadingInvestigation, setHandleOpenIndividualModal, setHandleDeleteInvestigation, } = useInvestigationStore() useEffect(() => { - setInvestigation(investigation) - setIsLoadingInvestigation(isLoadingInvestigation) - - const createQueryString = (name: string, value: string) => { - const params = new URLSearchParams(searchParams.toString()) - params.set(name, value) - return params.toString() - } - - const handleOpenIndividualModal = (id: string) => - router.push(pathname + "?" + createQueryString("individual_id", id)) - - setHandleOpenIndividualModal(() => handleOpenIndividualModal) - const handleDeleteInvestigation = async () => { - if ( - await confirm({ - title: "Delete investigation", - message: "Are you really sure you want to delete this investigation ?", - }) - ) { - if ( - await confirm({ - title: "Just making sure", - message: "You will definitely delete all nodes, edges and relationships.", - }) - ) { - const { error } = await supabase.from("investigations").delete().eq("id", investigation_id) - if (error) throw error - return router.push("/dashboard") - } + const confirmDelete = await confirm({ + title: "Delete investigation", + message: "Are you really sure you want to delete this investigation ?", + }) + + if (!confirmDelete) return + + const confirmFinal = await confirm({ + title: "Just making sure", + message: "You will definitely delete all nodes, edges and relationships.", + }) + if (!confirmFinal) return + try { + const { error } = await supabase + .from("investigations") + .delete() + .eq("id", investigation_id) + + if (error) throw error + + router.push("/dashboard") + } catch (error) { + console.error("Error deleting investigation:", error) } } - setHandleDeleteInvestigation(handleDeleteInvestigation) }, [ - investigation, - isLoadingInvestigation, investigation_id, router, pathname, searchParams, confirm, - setInvestigation, - setIsLoadingInvestigation, setHandleOpenIndividualModal, setHandleDeleteInvestigation, ]) @@ -90,7 +73,13 @@ export const InvestigationProvider: React.FC = ({ ch title, description, disabled = false, - }: { setting: string; value: boolean; title: string; description: string; disabled?: boolean }) => ( + }: { + setting: keyof typeof settings + value: boolean + title: string + description: string + disabled?: boolean + }) => (

{title}

@@ -99,7 +88,9 @@ export const InvestigationProvider: React.FC = ({ ch setSettings({ ...settings, [setting]: val })} + onCheckedChange={(val: boolean) => + setSettings({ ...settings, [setting]: val }) + } />
) @@ -113,34 +104,34 @@ export const InvestigationProvider: React.FC = ({ ch Make changes to your settings.
@@ -156,5 +147,4 @@ export const InvestigationProvider: React.FC = ({ ch ) } -export const useInvestigationContext = useInvestigationStore - +export const useInvestigationContext = useInvestigationStore \ No newline at end of file diff --git a/src/components/contexts/search-context.tsx b/src/components/contexts/search-context.tsx index 97a60ab..506f770 100644 --- a/src/components/contexts/search-context.tsx +++ b/src/components/contexts/search-context.tsx @@ -44,7 +44,7 @@ export const SearchProvider: React.FC = ({ children }) => { setIsLoading(true) setError("") try { - const data = await investigateValue(investigation_id as string, value) + const data = await investigateValue(value) setResults(data) } catch (err) { setError(err instanceof Error ? err.message : "An error occurred.") diff --git a/src/components/investigations/case-selector.tsx b/src/components/investigations/case-selector.tsx index 7bed30c..94068af 100644 --- a/src/components/investigations/case-selector.tsx +++ b/src/components/investigations/case-selector.tsx @@ -1,6 +1,6 @@ import { useInvestigations } from "@/lib/hooks/investigation/investigation"; import { useInvestigationStore } from '@/store/investigation-store'; -import { useRouter } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; import { Select, SelectContent, @@ -12,17 +12,22 @@ import { Skeleton } from "@/components/ui/skeleton"; export default function CaseSelector() { const router = useRouter() + const { investigation_id } = useParams() + const useInvestigationData = useInvestigationStore( + (state) => state.useInvestigationData + ); const { investigations, isLoading } = useInvestigations() - const { investigation, isLoadingInvestigation } = useInvestigationStore() + const { investigation } = useInvestigationData(investigation_id as string); + const handleSelectionChange = (value: string) => { router.push(`/investigations/${value}`); }; return (
- {isLoading || isLoadingInvestigation ? : - - + {investigations?.map((investigation) => ( diff --git a/src/components/investigations/graph.tsx b/src/components/investigations/graph.tsx index cdbf0c2..2d05106 100644 --- a/src/components/investigations/graph.tsx +++ b/src/components/investigations/graph.tsx @@ -43,7 +43,7 @@ const nodeTypes = { address: AddressNode }; -const LayoutFlow = ({ theme }: { theme: ColorMode }) => { +const LayoutFlow = ({ refetch, theme }: { refetch: any, theme: ColorMode }) => { const { fitView, zoomIn, zoomOut, addNodes, getNode, setCenter } = useReactFlow(); const { investigation_id } = useParams(); const { settings } = useInvestigationStore(); @@ -59,7 +59,6 @@ const LayoutFlow = ({ theme }: { theme: ColorMode }) => { onPaneClick, currentNode, resetNodeStyles, - refreshData, reloading } = useFlowStore(); @@ -133,7 +132,7 @@ const LayoutFlow = ({ theme }: { theme: ColorMode }) => {
- @@ -190,19 +189,21 @@ const LayoutFlow = ({ theme }: { theme: ColorMode }) => { ); }; -export default function Graph({ initialNodes, initialEdges }: { initialNodes: any, initialEdges: any }) { +export default function Graph({ graphQuery }: { graphQuery: any }) { const [mounted, setMounted] = useState(false); + const { refetch, isLoading, data } = graphQuery const { resolvedTheme } = useTheme(); useEffect(() => { setMounted(true); }, []); - useEffect(() => { - useFlowStore.setState({ nodes: initialNodes, edges: initialEdges }); - setMounted(true); - }, [initialNodes, initialEdges]); + if (data) { + useFlowStore.setState({ nodes: data?.nodes, edges: data?.edges }); + setMounted(true); + } + }, [data, data?.nodes, data?.edges]); - if (!mounted) { + if (!mounted || isLoading) { return (
Loading... @@ -212,7 +213,7 @@ export default function Graph({ initialNodes, initialEdges }: { initialNodes: an return ( - + ); } \ No newline at end of file diff --git a/src/components/investigations/individual-modal.tsx b/src/components/investigations/individual-modal.tsx index dedff94..1cb9aab 100644 --- a/src/components/investigations/individual-modal.tsx +++ b/src/components/investigations/individual-modal.tsx @@ -21,7 +21,6 @@ import { useQueryState } from "nuqs" const IndividualModal = () => { const [individualId, setIndividualId] = useQueryState("individual_id") - const { handleOpenIndividualModal } = useInvestigationStore() const { individual, isLoading } = useIndividual(individualId) const { emails, isLoading: isLoadingEmails } = useEmailsAndBreaches(individualId) const platformsIcons = usePlatformIcons("medium") diff --git a/src/components/investigations/nodes/email.tsx b/src/components/investigations/nodes/email.tsx index 9a19150..3874c83 100644 --- a/src/components/investigations/nodes/email.tsx +++ b/src/components/investigations/nodes/email.tsx @@ -69,7 +69,7 @@ function EmailNode({ data }: any) {
diff --git a/src/components/investigations/nodes/ip_address.tsx b/src/components/investigations/nodes/ip_address.tsx index fa34bb4..dbc6276 100644 --- a/src/components/investigations/nodes/ip_address.tsx +++ b/src/components/investigations/nodes/ip_address.tsx @@ -69,7 +69,7 @@ function IpNode({ data }: any) {
diff --git a/src/components/investigations/nodes/phone.tsx b/src/components/investigations/nodes/phone.tsx index d357341..c5f3af3 100644 --- a/src/components/investigations/nodes/phone.tsx +++ b/src/components/investigations/nodes/phone.tsx @@ -69,7 +69,7 @@ function PhoneNode({ data }: any) {
diff --git a/src/components/investigations/nodes/physical_address.tsx b/src/components/investigations/nodes/physical_address.tsx index 0008e7b..f4fbf66 100644 --- a/src/components/investigations/nodes/physical_address.tsx +++ b/src/components/investigations/nodes/physical_address.tsx @@ -69,7 +69,7 @@ function AddressNode({ data }: any) {
diff --git a/src/components/investigations/nodes/social.tsx b/src/components/investigations/nodes/social.tsx index 745975a..06f5d31 100644 --- a/src/components/investigations/nodes/social.tsx +++ b/src/components/investigations/nodes/social.tsx @@ -74,7 +74,7 @@ function SocialNode({ data }: any) {
diff --git a/src/lib/actions/investigations.ts b/src/lib/actions/investigations.ts index 7dd09d9..24bcb99 100644 --- a/src/lib/actions/investigations.ts +++ b/src/lib/actions/investigations.ts @@ -1,22 +1,14 @@ "use server" import { createClient } from "../supabase/server" -import { Investigation } from "@/types/investigation" -import { NodeData, EdgeData } from "@/types" -import { notFound, redirect } from "next/navigation" +import { redirect } from "next/navigation" import { revalidatePath } from "next/cache" -interface ReturnTypeGetInvestigations { - investigations: Investigation[], - error?: any -} - export async function createNewCase(formData: FormData) { const supabase = await createClient() const { data: session, error: userError } = await supabase.auth.getUser() if (userError || !session?.user) { redirect('/login') } - const data = { title: formData.get("title"), description: formData.get("description"), @@ -31,164 +23,3 @@ export async function createNewCase(formData: FormData) { return { success: false, error: "Failed to create new case" } } } - -export async function getInvestigations(): Promise { - const supabase = await createClient() - let { data: investigations, error } = await supabase - .from('investigations') - .select('id, title, description') - return { investigations: investigations as Investigation[], error: error } -} -interface ReturnTypeGetInvestigation { - investigation: Investigation, - error?: any -} -export async function getInvestigation(investigationId: string): Promise { - const supabase = await createClient() - let { data: investigation, error } = await supabase - .from('investigations') - .select('id, title, description') - .eq("id", investigationId) - .single() - return { investigation: investigation as Investigation, error: error } -} - -export async function getInvestigationData(investigationId: string): Promise<{ nodes: NodeData[], edges: EdgeData[] }> { - const supabase = await createClient(); - let { data: individuals, error: indError } = await supabase - .from('individuals') - .select('*, ip_addresses(*), phone_numbers(*), social_accounts(*), emails(*), physical_addresses(*)') - .eq('investigation_id', investigationId); - if (indError) throw notFound(); - - if (!individuals) individuals = []; - // Extraire les IDs - // @ts-ignore - const individualIds = individuals.map((ind) => ind.id); - - if (individualIds.length === 0) { - return { nodes: [], edges: [] }; - } - - let { data: relations, error: relError } = await supabase - .from('relationships') - .select('individual_a, individual_b, relation_type, confidence_level') - .in('individual_a', individualIds) - .in('individual_b', individualIds); - - if (relError) throw relError; - if (!relations) relations = []; - - const nodes: NodeData[] = []; - const edges: EdgeData[] = []; - - // Construire les nœuds des individus - individuals.forEach((ind: any) => { - const individualId = ind.id.toString(); - nodes.push({ - id: individualId, - type: 'individual', - data: { ...ind, label: ind.full_name }, - position: { x: 0, y: 100 } - }); - - // Ajouter les emails - ind.emails?.forEach((email: any) => { - nodes.push({ - id: email.id.toString(), - type: 'email', - data: { ...email, label: email.email }, - position: { x: 100, y: 100 } - }); - edges.push({ - source: individualId, - target: email.id.toString(), - type: 'custom', - id: `${individualId}-${email.id}`.toString(), - label: 'email', - }); - }); - - // Ajouter les numéros de téléphone - ind.phone_numbers?.forEach((phone: any) => { - nodes.push({ - id: phone.id.toString(), - type: 'phone', - data: { ...phone, label: phone.phone_number }, - position: { x: -100, y: 100 } - }); - edges.push({ - source: individualId, - target: phone.id.toString(), - type: 'custom', - id: `${individualId}-${phone.id}`.toString(), - label: 'phone', - }); - }); - - // Ajouter les comptes sociaux - ind.social_accounts?.forEach((social: any) => { - nodes.push({ - id: social.id.toString(), - type: 'social', - data: { ...social, label: `${social.platform}: ${social.username}` }, - position: { x: 100, y: -100 } - }); - edges.push({ - source: individualId, - target: social.id.toString(), - type: 'custom', - id: `${individualId}-${social.id}`.toString(), - label: 'social', - }); - }); - - // Ajouter les adresses IP - ind.ip_addresses?.forEach((ip: any) => { - nodes.push({ - id: ip.id.toString(), - type: 'ip', - data: { label: ip.ip_address }, - position: { x: -100, y: -100 } - }); - edges.push({ - source: individualId, - target: ip.id.toString(), - type: 'custom', - id: `${individualId}-${ip.id}`.toString(), - label: 'IP', - }); - }); - - ind.physical_addresses?.forEach((address: any) => { - nodes.push({ - id: address.id.toString(), - type: 'address', - data: { ...address, label: [address.address, address.city, address.country].join(", ") }, - position: { x: 100, y: 100 } - }); - edges.push({ - source: individualId, - target: address.id.toString(), - type: 'custom', - id: `${individualId}-${address.id}`.toString(), - label: 'address', - }); - }); - }); - relations.forEach(({ individual_a, individual_b, relation_type, confidence_level }) => { - edges.push({ - source: individual_a.toString(), - target: individual_b.toString(), - type: 'custom', - id: `${individual_a}-${individual_b}`.toString(), - label: relation_type, - confidence_level: confidence_level - }); - }); - - return { nodes, edges }; -} - - - diff --git a/src/lib/actions/search.ts b/src/lib/actions/search.ts index 04570ca..36343a3 100644 --- a/src/lib/actions/search.ts +++ b/src/lib/actions/search.ts @@ -1,6 +1,5 @@ 'use server' -import { getInvestigation } from "@/lib/actions/investigations" import { notFound } from "next/navigation" import { createClient } from "../supabase/server"; @@ -56,14 +55,10 @@ async function checkBreachedAccount(account: string | number | boolean, apiKey: const apiKey = process.env.HIBP_API_KEY || "" const appName = 'MyHIBPChecker'; -export async function investigateValue(investigation_id: any, username: string) { +export async function investigateValue(username: string) { if (!username) { throw new Error("Malformed query.") } - const { investigation, error } = await getInvestigation(investigation_id) - if (error || !investigation) { - notFound() - } return checkBreachedAccount(username, apiKey, appName); } diff --git a/src/lib/supabase/middleware.ts b/src/lib/supabase/middleware.ts index 9808ac5..304b266 100644 --- a/src/lib/supabase/middleware.ts +++ b/src/lib/supabase/middleware.ts @@ -27,10 +27,12 @@ export async function updateSession(request: NextRequest) { } ) - // IMPORTANT: Avoid writing any logic between createServerClient and + // Do not run code between createServerClient and // supabase.auth.getUser(). A simple mistake could make it very hard to debug // issues with users being randomly logged out. + // IMPORTANT: DO NOT REMOVE auth.getUser() + const { data: { user }, } = await supabase.auth.getUser() @@ -38,7 +40,8 @@ export async function updateSession(request: NextRequest) { if ( !user && !request.nextUrl.pathname.startsWith('/login') && - !request.nextUrl.pathname.startsWith('/auth') + !request.nextUrl.pathname.startsWith('/auth') && + !request.nextUrl.pathname.startsWith('/api') ) { // no user, potentially respond by redirecting the user to the login page const url = request.nextUrl.clone() @@ -46,8 +49,8 @@ export async function updateSession(request: NextRequest) { return NextResponse.redirect(url) } - // IMPORTANT: You *must* return the supabaseResponse object as it is. If you're - // creating a new response object with NextResponse.next() make sure to: + // IMPORTANT: You *must* return the supabaseResponse object as it is. + // If you're creating a new response object with NextResponse.next() make sure to: // 1. Pass the request in it, like so: // const myNewResponse = NextResponse.next({ request }) // 2. Copy over the cookies, like so: diff --git a/src/lib/supabase/server.ts b/src/lib/supabase/server.ts index a46cfa6..f90cd3a 100644 --- a/src/lib/supabase/server.ts +++ b/src/lib/supabase/server.ts @@ -3,11 +3,25 @@ import { cookies } from 'next/headers' export async function createClient() { const cookieStore = await cookies() + const supabaseToken = cookieStore.get('sb-token')?.value return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { + auth: { + persistSession: false, + // Add the token if it exists + ...(supabaseToken && { + autoRefreshToken: false, + detectSessionInUrl: false, + storage: { + getItem: () => supabaseToken, + setItem: () => { }, + removeItem: () => { }, + }, + }), + }, cookies: { getAll() { return cookieStore.getAll() diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..0606913 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,18 @@ +import { updateSession } from '@/lib/supabase/middleware' +import { NextRequest } from 'next/server' + +export async function middleware(request: NextRequest) { + return await updateSession(request) +} +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + * Feel free to modify this pattern to include more paths. + */ + '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', + ], +} \ No newline at end of file diff --git a/src/store/flow-store.ts b/src/store/flow-store.ts index 53bfcde..f4a1de2 100644 --- a/src/store/flow-store.ts +++ b/src/store/flow-store.ts @@ -8,7 +8,6 @@ import { type OnNodesChange, type OnEdgesChange, } from '@xyflow/react'; -import { getInvestigationData } from '@/lib/actions/investigations'; export type AppNode = Node; @@ -29,7 +28,6 @@ export type AppState = { setCurrentNode: (nodeId: string | null) => void; updateNode: (nodeId: string, nodeData: Partial) => void; resetNodeStyles: () => void; - refreshData: (Investigation_id: string, fitView: () => void) => void, }; const getLayoutedElements = (nodes: any[], edges: any[], options: { direction: any; }) => { const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); @@ -206,19 +204,6 @@ const createStore = (initialNodes: AppNode[] = [], initialEdges: Edge[] = []) => }) }); }, - refreshData: async (investigation_id: string, fitView: () => void) => { - set({ reloading: true }); - try { - const { nodes: newNodes, edges: newEdges } = await getInvestigationData(investigation_id); - set({ nodes: newNodes, edges: newEdges }); - get().onLayout('LR', fitView); - } catch (error) { - console.error('Error refreshing data:', error); - set({ reloading: false }); - } finally { - set({ reloading: false }); - } - }, })) } export const useFlowStore = createStore(); diff --git a/src/store/investigation-store.ts b/src/store/investigation-store.ts index 78f69cc..9edacfe 100644 --- a/src/store/investigation-store.ts +++ b/src/store/investigation-store.ts @@ -1,67 +1,70 @@ "use client" -import React from "react" - import { create } from "zustand" import { persist, createJSONStorage } from "zustand/middleware" -import type { Investigation } from "@/types/investigation" -import { useIndividuals } from "@/lib/hooks/investigation/use-individuals" -import { useEmails } from "@/lib/hooks/investigation/use-emails" -import { usePhones } from "@/lib/hooks/investigation/use-phones" -import { useSocials } from "@/lib/hooks/investigation/use-socials" +import { useQuery, useQueryClient, type QueryObserverResult } from "@tanstack/react-query" +import type { Investigation, Individual, Email, Phone, Social, IP, Relation, Address } from "@/types/investigation" + +// Define a generic type for query results +interface QueryResult { + data: T + isLoading: boolean + refetch: () => Promise +} + +// Define the return type for useInvestigationData +interface InvestigationData { + investigation: QueryResult + individuals: QueryResult + emails: QueryResult + phones: QueryResult + socials: QueryResult + ips: QueryResult + addresses: QueryResult + relations: QueryResult +} interface InvestigationState { + // UI State filters: any - setFilters: (filters: any) => void - settings: any - setSettings: (settings: any) => void + settings: { + showNodeLabel: boolean + showEdgeLabel: boolean + showMiniMap: boolean + showCopyIcon: boolean + showNodeToolbar: boolean + } openSettingsModal: boolean - setOpenSettingsModal: (open: boolean) => void + currentNode: any + panelOpen: boolean investigation: Investigation | null + + // UI Actions + setFilters: (filters: any) => void + setSettings: (settings: any) => void + setOpenSettingsModal: (open: boolean) => void + setCurrentNode: (node: any) => void + setPanelOpen: (open: boolean) => void setInvestigation: (investigation: Investigation | null) => void - isLoadingInvestigation: boolean | undefined - setIsLoadingInvestigation: (isLoading: boolean | undefined) => void + + // Modal Handlers handleOpenIndividualModal: (id: string) => void setHandleOpenIndividualModal: (handler: (id: string) => void) => void handleDeleteInvestigation: () => Promise setHandleDeleteInvestigation: (handler: () => Promise) => void - currentNode: any - setCurrentNode: (node: any) => void - panelOpen: boolean - setPanelOpen: (open: boolean) => void - individuals: any[] - setIndividuals: (individuals: any[]) => void - isLoadingIndividuals: boolean - setIsLoadingIndividuals: (isLoading: boolean) => void - refetchIndividuals: () => void - setRefetchIndividuals: (refetch: () => void) => void - emails: any[] - setEmails: (emails: any[]) => void - isLoadingEmails: boolean - setIsLoadingEmails: (isLoading: boolean) => void - refetchEmails: () => void - setRefetchEmails: (refetch: () => void) => void - phones: any[] - setPhones: (phones: any[]) => void - isLoadingPhones: boolean - setIsLoadingPhones: (isLoading: boolean) => void - refetchPhones: () => void - setRefetchPhones: (refetch: () => void) => void - socials: any[] - setSocials: (socials: any[]) => void - isLoadingSocials: boolean - setIsLoadingSocials: (isLoading: boolean) => void - refetchSocials: () => void - setRefetchSocials: (refetch: () => void) => void + + // Query Hooks + useInvestigationData: (investigationId: string) => InvestigationData + refetchAll: (investigationId: string) => Promise } const isServer = typeof window === "undefined" export const useInvestigationStore = create( persist( - (set, get) => ({ + (set, _) => ({ + // UI State filters: {}, - setFilters: (filters) => set({ filters }), settings: { showNodeLabel: true, showEdgeLabel: true, @@ -69,114 +72,127 @@ export const useInvestigationStore = create( showCopyIcon: true, showNodeToolbar: true, }, - setSettings: (settings) => set({ settings }), openSettingsModal: false, - setOpenSettingsModal: (open) => set({ openSettingsModal: open }), + currentNode: null, + panelOpen: false, investigation: null, + + // UI Actions + setFilters: (filters) => set({ filters }), + setSettings: (settings) => set({ settings }), + setOpenSettingsModal: (open) => set({ openSettingsModal: open }), + setCurrentNode: (node) => set({ currentNode: node }), + setPanelOpen: (open) => set({ panelOpen: open }), setInvestigation: (investigation) => set({ investigation }), - isLoadingInvestigation: undefined, - setIsLoadingInvestigation: (isLoading) => set({ isLoadingInvestigation: isLoading }), handleOpenIndividualModal: () => { }, setHandleOpenIndividualModal: (handler) => set({ handleOpenIndividualModal: handler }), handleDeleteInvestigation: async () => { }, setHandleDeleteInvestigation: (handler) => set({ handleDeleteInvestigation: handler }), - currentNode: null, - setCurrentNode: (node) => set({ currentNode: node }), - panelOpen: false, - setPanelOpen: (open) => set({ panelOpen: open }), - individuals: [], - setIndividuals: (individuals) => set({ individuals }), - isLoadingIndividuals: true, - setIsLoadingIndividuals: (isLoading) => set({ isLoadingIndividuals: isLoading }), - refetchIndividuals: () => { }, - setRefetchIndividuals: (refetch) => set({ refetchIndividuals: refetch }), - emails: [], - setEmails: (emails) => set({ emails }), - isLoadingEmails: true, - setIsLoadingEmails: (isLoading) => set({ isLoadingEmails: isLoading }), - refetchEmails: () => { }, - setRefetchEmails: (refetch) => set({ refetchEmails: refetch }), - phones: [], - setPhones: (phones) => set({ phones }), - isLoadingPhones: true, - setIsLoadingPhones: (isLoading) => set({ isLoadingPhones: isLoading }), - refetchPhones: () => { }, - setRefetchPhones: (refetch) => set({ refetchPhones: refetch }), - socials: [], - setSocials: (socials) => set({ socials }), - isLoadingSocials: true, - setIsLoadingSocials: (isLoading) => set({ isLoadingSocials: isLoading }), - refetchSocials: () => { }, - setRefetchSocials: (refetch) => set({ refetchSocials: refetch }), + useInvestigationData: (investigationId: string) => { + const wrapRefetch = async (refetch: () => Promise) => { + await refetch() + } + const investigationQuery = useQuery({ + queryKey: ["investigation", investigationId, 'investigation'], + queryFn: async () => { + const res = await fetch(`/api/investigations/${investigationId}`) + if (!res.ok) throw new Error("Failed to fetch investigation") + const data = await res.json() + return data.investigation as Investigation + }, + }) + const individualsQuery = useQuery({ + queryKey: ["investigation", investigationId, "individuals"], + queryFn: async () => { + const res = await fetch(`/api/investigations/${investigationId}/individuals`) + if (!res.ok) throw new Error("Failed to fetch individuals") + const data = await res.json() + return data.individuals + }, + }) + const emailsQuery = useQuery({ + queryKey: ["investigation", investigationId, "emails"], + queryFn: async () => { + const res = await fetch(`/api/investigations/${investigationId}/emails`) + if (!res.ok) throw new Error("Failed to fetch emails") + const data = await res.json() + return data.emails + }, + }) + const phonesQuery = useQuery({ + queryKey: ["investigation", investigationId, "phones"], + queryFn: async () => { + const res = await fetch(`/api/investigations/${investigationId}/phones`) + if (!res.ok) throw new Error("Failed to fetch phones") + const data = await res.json() + return data.phones + }, + }) + return { + investigation: { + data: investigationQuery.data as Investigation, + isLoading: investigationQuery.isLoading, + refetch: () => wrapRefetch(investigationQuery.refetch), + }, + individuals: { + data: individualsQuery.data ?? [], + isLoading: individualsQuery.isLoading, + refetch: () => wrapRefetch(individualsQuery.refetch), + }, + emails: { + data: emailsQuery.data ?? [], + isLoading: emailsQuery.isLoading, + refetch: () => wrapRefetch(emailsQuery.refetch), + }, + phones: { + data: phonesQuery.data ?? [], + isLoading: phonesQuery.isLoading, + refetch: () => wrapRefetch(phonesQuery.refetch), + }, + socials: { + data: [], + isLoading: false, + refetch: async () => { }, + }, + ips: { + data: [], + isLoading: false, + refetch: async () => { }, + }, + addresses: { + data: [], + isLoading: false, + refetch: async () => { }, + }, + relations: { + data: [], + isLoading: false, + refetch: async () => { }, + }, + } + }, + refetchAll: async (investigationId: string) => { + const queryClient = useQueryClient() + await Promise.all([ + queryClient.invalidateQueries({ + queryKey: ["investigation", investigationId, "individuals"] + }), + queryClient.invalidateQueries({ + queryKey: ["investigation", investigationId, "emails"] + }), + queryClient.invalidateQueries({ + queryKey: ["investigation", investigationId, "phones"] + }), + ]) + }, }), { name: "investigation-storage", storage: createJSONStorage(() => (isServer ? localStorage : localStorage)), - // @ts-ignore partialize: (state) => ({ - // Only persist these fields - filters: state.filters, - settings: state.settings, - investigation: state.investigation, - individuals: state.individuals, - emails: state.emails, - phones: state.phones, - socials: state.socials + ...state, }), }, - ), + ) ) -export const useInvestigationData = (investigation_id: string) => { - const { - setIndividuals, - setIsLoadingIndividuals, - setRefetchIndividuals, - setEmails, - setIsLoadingEmails, - setRefetchEmails, - setPhones, - setIsLoadingPhones, - setRefetchPhones, - setSocials, - setIsLoadingSocials, - setRefetchSocials, - } = useInvestigationStore() - - const { individuals, isLoading: isLoadingIndividuals, refetch: refetchIndividuals } = useIndividuals(investigation_id) - const { emails, isLoading: isLoadingEmails, refetch: refetchEmails } = useEmails(investigation_id) - const { phones, isLoading: isLoadingPhones, refetch: refetchPhones } = usePhones(investigation_id) - const { socials, isLoading: isLoadingSocials, refetch: refetchSocials } = useSocials(investigation_id) - - React.useEffect(() => { - setIndividuals(individuals || []) - setIsLoadingIndividuals(isLoadingIndividuals) - setRefetchIndividuals(() => refetchIndividuals) - }, [ - individuals, - isLoadingIndividuals, - refetchIndividuals, - setIndividuals, - setIsLoadingIndividuals, - setRefetchIndividuals, - ]) - - React.useEffect(() => { - setEmails(emails || []) - setIsLoadingEmails(isLoadingEmails) - setRefetchEmails(() => refetchEmails) - }, [emails, isLoadingEmails, refetchEmails, setEmails, setIsLoadingEmails, setRefetchEmails]) - - React.useEffect(() => { - setPhones(phones || []) - setIsLoadingPhones(isLoadingPhones) - setRefetchPhones(() => refetchPhones) - }, [phones, isLoadingPhones, refetchPhones, setPhones, setIsLoadingPhones, setRefetchPhones]) - - React.useEffect(() => { - setSocials(socials || []) - setIsLoadingSocials(isLoadingSocials) - setRefetchSocials(() => refetchSocials) - }, [socials, isLoadingSocials, refetchSocials, setSocials, setIsLoadingSocials, setRefetchSocials]) -} - diff --git a/src/types/investigation.ts b/src/types/investigation.ts index e64eeee..22d479c 100644 --- a/src/types/investigation.ts +++ b/src/types/investigation.ts @@ -2,4 +2,42 @@ export interface Investigation { id: string title: string description: string -} \ No newline at end of file +} +export interface Individual { + id: string + full_name: string +} + +export interface Email { + id: string + email: string +} + +export interface Phone { + id: string, + phone_number: string +} + +export interface Social { + id: string + profile_url: string + username: string + platform: string +} + +export interface IP { + id: string + ip_address: string +} + +export interface Address { + id: string + address: string + city: string + country: string + zip: string +} + +export interface Relation { + id: string +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index e3840d4..45e4d80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1568,6 +1568,18 @@ postcss "^8.4.41" tailwindcss "4.0.3" +"@tanstack/query-core@5.66.4": + version "5.66.4" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.66.4.tgz#44b87bff289466adbfa0de8daa5756cbd2d61c61" + integrity sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA== + +"@tanstack/react-query@^5.66.8": + version "5.66.8" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.66.8.tgz#a27494a4ce69c88363eee33637a57e013d7dd5c1" + integrity sha512-LqYHYArmM7ycyT1I/Txc/n6KzI8S/hBFw2SQ9Uj1GpbZ89AvZLEvetquiQEHkZ5rFEm+iVNpZ6zYjTiPmJ9N5Q== + dependencies: + "@tanstack/query-core" "5.66.4" + "@types/cookie@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5"