From beac4222050c799b1df6be3794611c623761e2fc Mon Sep 17 00:00:00 2001 From: Recoup Agent Date: Tue, 17 Mar 2026 13:56:22 +0000 Subject: [PATCH] feat: add Privy logins page to admin dashboard New /privy page showing daily/weekly/monthly login counts and a table of individual logins with email, Privy DID, and timestamp. Adds "View Privy Logins" nav button to AdminDashboard. Co-Authored-By: Claude Sonnet 4.6 --- app/privy/page.tsx | 10 +++ components/Home/AdminDashboard.tsx | 1 + components/PrivyLogins/PrivyLoginsPage.tsx | 83 +++++++++++++++++++++ components/PrivyLogins/PrivyLoginsTable.tsx | 42 +++++++++++ hooks/usePrivyLogins.ts | 24 ++++++ lib/recoup/fetchPrivyLogins.ts | 29 +++++++ types/privy.ts | 13 ++++ 7 files changed, 202 insertions(+) create mode 100644 app/privy/page.tsx create mode 100644 components/PrivyLogins/PrivyLoginsPage.tsx create mode 100644 components/PrivyLogins/PrivyLoginsTable.tsx create mode 100644 hooks/usePrivyLogins.ts create mode 100644 lib/recoup/fetchPrivyLogins.ts create mode 100644 types/privy.ts diff --git a/app/privy/page.tsx b/app/privy/page.tsx new file mode 100644 index 0000000..585b62a --- /dev/null +++ b/app/privy/page.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from "next"; +import PrivyLoginsPage from "@/components/PrivyLogins/PrivyLoginsPage"; + +export const metadata: Metadata = { + title: "Privy Logins — Recoup Admin", +}; + +export default function Page() { + return ; +} diff --git a/components/Home/AdminDashboard.tsx b/components/Home/AdminDashboard.tsx index 63e9617..32ac9e8 100644 --- a/components/Home/AdminDashboard.tsx +++ b/components/Home/AdminDashboard.tsx @@ -11,6 +11,7 @@ export default function AdminDashboard() { ); diff --git a/components/PrivyLogins/PrivyLoginsPage.tsx b/components/PrivyLogins/PrivyLoginsPage.tsx new file mode 100644 index 0000000..077d407 --- /dev/null +++ b/components/PrivyLogins/PrivyLoginsPage.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { useState } from "react"; +import PageBreadcrumb from "@/components/Sandboxes/PageBreadcrumb"; +import ApiDocsLink from "@/components/ApiDocsLink"; +import { usePrivyLogins } from "@/hooks/usePrivyLogins"; +import PrivyLoginsTable from "@/components/PrivyLogins/PrivyLoginsTable"; +import type { PrivyLoginsPeriod } from "@/types/privy"; + +const PERIODS: { value: PrivyLoginsPeriod; label: string }[] = [ + { value: "daily", label: "Daily" }, + { value: "weekly", label: "Weekly" }, + { value: "monthly", label: "Monthly" }, +]; + +export default function PrivyLoginsPage() { + const [period, setPeriod] = useState("daily"); + const { data, isLoading, error } = usePrivyLogins(period); + + return ( +
+
+
+ +

+ Privy Logins +

+

+ User sign-ins via Privy, grouped by time period. +

+
+ +
+ +
+
+ {PERIODS.map(({ value, label }) => ( + + ))} +
+ + {data && ( + + {data.total}{" "} + login{data.total !== 1 ? "s" : ""} + + )} +
+ + {isLoading && ( +
+ Loading logins… +
+ )} + + {error && ( +
+ {error instanceof Error ? error.message : "Failed to load Privy logins"} +
+ )} + + {!isLoading && !error && data && data.logins.length === 0 && ( +
+ No logins found for this period. +
+ )} + + {!isLoading && !error && data && data.logins.length > 0 && ( + + )} +
+ ); +} diff --git a/components/PrivyLogins/PrivyLoginsTable.tsx b/components/PrivyLogins/PrivyLoginsTable.tsx new file mode 100644 index 0000000..acc97e4 --- /dev/null +++ b/components/PrivyLogins/PrivyLoginsTable.tsx @@ -0,0 +1,42 @@ +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import type { PrivyLoginRow } from "@/types/privy"; + +interface PrivyLoginsTableProps { + logins: PrivyLoginRow[]; +} + +export default function PrivyLoginsTable({ logins }: PrivyLoginsTableProps) { + return ( +
+ + + + Email + Privy DID + Created At + + + + {logins.map((login) => ( + + + {login.email ?? No email} + + {login.privy_did} + + {new Date(login.created_at).toLocaleString()} + + + ))} + +
+
+ ); +} diff --git a/hooks/usePrivyLogins.ts b/hooks/usePrivyLogins.ts new file mode 100644 index 0000000..ee63b9f --- /dev/null +++ b/hooks/usePrivyLogins.ts @@ -0,0 +1,24 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { usePrivy } from "@privy-io/react-auth"; +import { fetchPrivyLogins } from "@/lib/recoup/fetchPrivyLogins"; +import type { PrivyLoginsPeriod } from "@/types/privy"; + +/** + * Fetches Privy login statistics for the given period from GET /api/admins/privy. + * Authenticates with the Privy access token (admin Bearer auth). + */ +export function usePrivyLogins(period: PrivyLoginsPeriod) { + const { ready, authenticated, getAccessToken } = usePrivy(); + + return useQuery({ + queryKey: ["admin", "privy", "logins", period], + queryFn: async () => { + const token = await getAccessToken(); + if (!token) throw new Error("Not authenticated"); + return fetchPrivyLogins(token, period); + }, + enabled: ready && authenticated, + }); +} diff --git a/lib/recoup/fetchPrivyLogins.ts b/lib/recoup/fetchPrivyLogins.ts new file mode 100644 index 0000000..874abc2 --- /dev/null +++ b/lib/recoup/fetchPrivyLogins.ts @@ -0,0 +1,29 @@ +import { API_BASE_URL } from "@/lib/consts"; +import type { PrivyLoginsPeriod, PrivyLoginsResponse } from "@/types/privy"; + +/** + * Fetches Privy login statistics from GET /api/admins/privy. + * Authenticates using the caller's Privy access token (admin Bearer auth). + * + * @param accessToken - Privy access token from getAccessToken() + * @param period - Time period: "daily", "weekly", or "monthly" + * @returns PrivyLoginsResponse with total count and login rows + */ +export async function fetchPrivyLogins( + accessToken: string, + period: PrivyLoginsPeriod, +): Promise { + const url = new URL(`${API_BASE_URL}/api/admins/privy`); + url.searchParams.set("period", period); + + const res = await fetch(url.toString(), { + headers: { Authorization: `Bearer ${accessToken}` }, + }); + + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error(body.error ?? body.message ?? `HTTP ${res.status}`); + } + + return res.json(); +} diff --git a/types/privy.ts b/types/privy.ts new file mode 100644 index 0000000..db52373 --- /dev/null +++ b/types/privy.ts @@ -0,0 +1,13 @@ +export type PrivyLoginsPeriod = "daily" | "weekly" | "monthly"; + +export type PrivyLoginRow = { + privy_did: string; + email: string | null; + created_at: string; +}; + +export type PrivyLoginsResponse = { + status: "success" | "error"; + total: number; + logins: PrivyLoginRow[]; +};