diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index c306de5..4177cbc 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -21,10 +21,19 @@ import { BreachTimeline } from "@/components/dashboard/BreachTimeline" import { TopBreaches } from "@/components/dashboard/TopBreaches" import { DataTypeRadar } from "@/components/dashboard/DataTypeRadar" import { AlertVelocity } from "@/components/dashboard/AlertVelocity" +import { redirect } from "next/navigation" import type { DashboardPreset } from "@/types/dashboard" export default async function DashboardPage() { const session = await auth() + + const [employeeCount, apiKeyCount] = await Promise.all([ + prisma.employee.count({ where: { companyId: session!.user.companyId } }), + prisma.apiCredential.count({ where: { companyId: session!.user.companyId } }), + ]) + + if (employeeCount === 0 && apiKeyCount === 0) redirect("/setup") + const [data, presets, user] = await Promise.all([ getDashboardData(session!.user.companyId), prisma.dashboardPreset.findMany({ diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 7fe2686..bc902f7 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -2,6 +2,7 @@ import { auth } from "@/auth" import { redirect } from "next/navigation" import { Sidebar } from "@/components/layout/Sidebar" import { Topbar } from "@/components/layout/Topbar" +import { RoutePrefetcher } from "@/components/layout/RoutePrefetcher" import { Providers } from "@/components/providers" import { getOpenAlertCount } from "@/lib/alerts" @@ -17,6 +18,7 @@ export default async function DashboardLayout({ return ( +
0 || apiKeyCount > 0) redirect("/dashboard") + + return ( + 0} + hasApiKey={apiKeyCount > 0} + isAdmin={session!.user.role === "ADMIN"} + /> + ) +} diff --git a/src/components/dashboard/SetupChecklist.tsx b/src/components/dashboard/SetupChecklist.tsx new file mode 100644 index 0000000..66c9fde --- /dev/null +++ b/src/components/dashboard/SetupChecklist.tsx @@ -0,0 +1,132 @@ +import Link from "next/link" +import { + CheckCircle2, + Circle, + Database, + KeyRound, + ScanSearch, + ArrowRight, + ExternalLink, +} from "lucide-react" +import { cn } from "@/lib/utils" + +type Step = { + icon: typeof Database + title: string + description: string + href: string + cta: string + done: boolean +} + +export function SetupChecklist({ + hasEmployees, + hasApiKey, + isAdmin, +}: { + hasEmployees: boolean + hasApiKey: boolean + isAdmin: boolean +}) { + const steps: Step[] = [ + { + icon: Database, + title: "Add employees to monitor", + description: "Connect a corporate directory (Azure AD, Google Workspace, LDAP...) to import the people you want to watch.", + href: "/data-sources", + cta: "Connect a data source", + done: hasEmployees, + }, + { + icon: KeyRound, + title: "Add a breach intelligence key", + description: "A provider key (HIBP, Dehashed...) lets DataShield look your employees up against known breaches.", + href: "/data-api", + cta: "Add an API key", + done: hasApiKey, + }, + { + icon: ScanSearch, + title: "Run your first scan", + description: "Once people and a key are in place, scan to detect exposures and generate alerts.", + href: "/employees", + cta: "Go to employees", + done: false, + }, + ] + + return ( +
+
+
+

Welcome to DataShield

+

+ Self-hosted monitoring of your employees exposure in known data breaches. Complete + these steps to start monitoring. +

+
+ +
    + {steps.map((step, i) => ( +
  1. +
    + {step.done ? ( + + ) : ( + + )} +
    +
    +
    + +

    + {i + 1}. {step.title} +

    +
    +

    {step.description}

    + {!step.done && ( + + {step.cta} + + + )} +
    +
  2. + ))} +
+ + {!isAdmin && ( +

+ Data sources and API keys are managed by admins. Ask an admin to complete the setup. +

+ )} + +
+

Thanks for trying DataShield.

+

+ It is an open-source project built to make breach exposure visible and actionable. + Found a bug or have an idea? Your feedback genuinely helps it grow. +

+ + Open an issue or share feedback + + +
+
+
+ ) +} diff --git a/src/components/layout/RoutePrefetcher.tsx b/src/components/layout/RoutePrefetcher.tsx new file mode 100644 index 0000000..37901be --- /dev/null +++ b/src/components/layout/RoutePrefetcher.tsx @@ -0,0 +1,31 @@ +"use client" + +import { useEffect } from "react" +import { useRouter } from "next/navigation" + +// Main routes warmed in the background after first paint so later +// navigation is instant. Keep in sync with the sidebar nav. +const ROUTES = [ + "/dashboard", + "/employees", + "/alerts", + "/reports", + "/data-sources", + "/data-api", +] + +export function RoutePrefetcher() { + const router = useRouter() + + useEffect(() => { + const run = () => ROUTES.forEach((route) => router.prefetch(route)) + if ("requestIdleCallback" in window) { + const id = requestIdleCallback(run) + return () => cancelIdleCallback(id) + } + const id = setTimeout(run, 200) + return () => clearTimeout(id) + }, [router]) + + return null +}