Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/app/(dashboard)/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
2 changes: 2 additions & 0 deletions src/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -17,6 +18,7 @@ export default async function DashboardLayout({

return (
<Providers>
<RoutePrefetcher />
<div className="flex h-screen overflow-hidden">
<Sidebar
companyName={session.user.name ?? ""}
Expand Down
24 changes: 24 additions & 0 deletions src/app/(dashboard)/setup/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { redirect } from "next/navigation"
import { auth } from "@/auth"
import { prisma } from "@/lib/prisma"
import { SetupChecklist } from "@/components/dashboard/SetupChecklist"

export default async function SetupPage() {
const session = await auth()
const companyId = session!.user.companyId

const [employeeCount, apiKeyCount] = await Promise.all([
prisma.employee.count({ where: { companyId } }),
prisma.apiCredential.count({ where: { companyId } }),
])

if (employeeCount > 0 || apiKeyCount > 0) redirect("/dashboard")

return (
<SetupChecklist
hasEmployees={employeeCount > 0}
hasApiKey={apiKeyCount > 0}
isAdmin={session!.user.role === "ADMIN"}
/>
)
}
132 changes: 132 additions & 0 deletions src/components/dashboard/SetupChecklist.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="h-full overflow-y-auto p-6">
<div className="mx-auto max-w-2xl">
<div className="mb-8">
<h2 className="text-xl font-semibold text-foreground">Welcome to DataShield</h2>
<p className="mt-1 text-sm text-muted-foreground">
Self-hosted monitoring of your employees exposure in known data breaches. Complete
these steps to start monitoring.
</p>
</div>

<ol className="space-y-3">
{steps.map((step, i) => (
<li
key={step.href}
className={cn(
"flex items-start gap-4 rounded-xl border border-border bg-card p-4",
step.done && "opacity-60"
)}
>
<div className="mt-0.5 shrink-0">
{step.done ? (
<CheckCircle2 className="size-5 text-severity-ok" />
) : (
<Circle className="size-5 text-muted-foreground" />
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<step.icon className="size-4 text-muted-foreground" />
<h3 className="text-sm font-medium text-foreground">
{i + 1}. {step.title}
</h3>
</div>
<p className="mt-1 text-sm text-muted-foreground">{step.description}</p>
{!step.done && (
<Link
href={step.href}
className="mt-2 inline-flex items-center gap-1 text-sm font-medium text-sidebar-primary hover:underline"
>
{step.cta}
<ArrowRight className="size-3.5" />
</Link>
)}
</div>
</li>
))}
</ol>

{!isAdmin && (
<p className="mt-4 text-xs text-muted-foreground">
Data sources and API keys are managed by admins. Ask an admin to complete the setup.
</p>
)}

<div className="mt-8 rounded-xl border border-border bg-muted/30 p-4">
<p className="text-sm text-foreground">Thanks for trying DataShield.</p>
<p className="mt-1 text-xs text-muted-foreground">
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.
</p>
<a
href="https://github.com/WhiteMuush/DataShield/issues"
target="_blank"
rel="noreferrer"
className="mt-2 inline-flex items-center gap-1 text-xs font-medium text-sidebar-primary hover:underline"
>
Open an issue or share feedback
<ExternalLink className="size-3" />
</a>
</div>
</div>
</div>
)
}
31 changes: 31 additions & 0 deletions src/components/layout/RoutePrefetcher.tsx
Original file line number Diff line number Diff line change
@@ -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
}