diff --git a/app/api/account-emails/route.ts b/app/api/account-emails/route.ts deleted file mode 100644 index 32f5fdf4a..000000000 --- a/app/api/account-emails/route.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import getAccountEmails from "@/lib/supabase/account_emails/getAccountEmails"; -import { checkAccountArtistAccess } from "@/lib/supabase/account_artist_ids/checkAccountArtistAccess"; - -export async function GET(req: NextRequest) { - const accountIds = req.nextUrl.searchParams.getAll("accountIds"); - const currentAccountId = req.nextUrl.searchParams.get("currentAccountId"); - const artistAccountId = req.nextUrl.searchParams.get("artistAccountId"); - - if (!currentAccountId || !artistAccountId) { - return NextResponse.json( - { error: "Missing authentication parameters" }, - { status: 400 } - ); - } - - if (!accountIds || accountIds.length === 0) { - return NextResponse.json([]); - } - - try { - // Verify current user has access to this artist - const hasAccess = await checkAccountArtistAccess( - currentAccountId, - artistAccountId - ); - - if (!hasAccess) { - return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); - } - - // Security model: If you can see tasks for this artist, you can see task creators' emails - // No additional filtering needed - the tasks query already scopes by artist access - const emails = await getAccountEmails(accountIds); - return NextResponse.json(emails); - } catch { - return NextResponse.json( - { error: "Failed to fetch account emails" }, - { status: 500 } - ); - } -} - -export const dynamic = "force-dynamic"; - diff --git a/components/Files/FileInfoDialog.tsx b/components/Files/FileInfoDialog.tsx index b743b446f..c36f44ccf 100644 --- a/components/Files/FileInfoDialog.tsx +++ b/components/Files/FileInfoDialog.tsx @@ -11,11 +11,8 @@ import { extractAccountIds } from "@/utils/extractAccountIds"; import FileInfoDialogHeader from "./FileInfoDialogHeader"; import FileInfoDialogContent from "./FileInfoDialogContent"; import FileInfoDialogMetadata from "./FileInfoDialogMetadata"; -import { useQuery } from "@tanstack/react-query"; -import { Tables } from "@/types/database.types"; import { useUserProvider } from "@/providers/UserProvder"; - -type AccountEmail = Tables<"account_emails">; +import { useAccountEmails } from "@/hooks/useAccountEmails"; type FileInfoDialogProps = { file: FileRow | null; @@ -23,34 +20,28 @@ type FileInfoDialogProps = { onOpenChange: (open: boolean) => void; }; -export default function FileInfoDialog({ file, open, onOpenChange }: FileInfoDialogProps) { +export default function FileInfoDialog({ + file, + open, + onOpenChange, +}: FileInfoDialogProps) { const { userData } = useUserProvider(); const { content } = useFileContent( - file?.file_name || "", - file?.storage_key || "", - userData?.account_id || "" + file?.file_name || "", + file?.storage_key || "", + userData?.account_id || "", ); - + // Extract account IDs and check if file is editable - const { ownerAccountId, artistAccountId } = file - ? extractAccountIds(file.storage_key) + const { ownerAccountId, artistAccountId } = file + ? extractAccountIds(file.storage_key) : { ownerAccountId: "", artistAccountId: "" }; const canEdit = file ? isTextFile(file.file_name) : false; // Fetch owner email - const { data: emails } = useQuery({ - queryKey: ["file-owner-email", ownerAccountId, artistAccountId], - queryFn: async () => { - if (!ownerAccountId || !artistAccountId || !userData) return []; - const params = new URLSearchParams(); - params.append("accountIds", ownerAccountId); - params.append("currentAccountId", userData.id); - params.append("artistAccountId", artistAccountId); - const response = await fetch(`/api/account-emails?${params}`); - if (!response.ok) return []; - return response.json(); - }, - enabled: !!ownerAccountId && !!artistAccountId && !!userData && open, + const { data: emails } = useAccountEmails({ + accountIds: ownerAccountId ? [ownerAccountId] : [], + enabled: open, }); const ownerEmail = emails?.[0]?.email || undefined; @@ -92,7 +83,7 @@ export default function FileInfoDialog({ file, open, onOpenChange }: FileInfoDia } baseHandleEditToggle(editing); }; - + return ( @@ -107,7 +98,7 @@ export default function FileInfoDialog({ file, open, onOpenChange }: FileInfoDia />
- ); } - diff --git a/components/TasksPage/TasksList.tsx b/components/TasksPage/TasksList.tsx index 9bd21c38c..15b6458d5 100644 --- a/components/TasksPage/TasksList.tsx +++ b/components/TasksPage/TasksList.tsx @@ -1,14 +1,10 @@ -import { Tables } from "@/types/database.types"; import { Task } from "@/lib/tasks/getTasks"; import TaskCard from "@/components/VercelChat/tools/tasks/TaskCard"; import TaskSkeleton from "./TaskSkeleton"; import TaskDetailsDialog from "@/components/VercelChat/dialogs/tasks/TaskDetailsDialog"; -import { useArtistProvider } from "@/providers/ArtistProvider"; import { useUserProvider } from "@/providers/UserProvder"; import { useMemo } from "react"; -import { useQuery } from "@tanstack/react-query"; - -type AccountEmail = Tables<"account_emails">; +import { useAccountEmails } from "@/hooks/useAccountEmails"; interface TasksListProps { tasks: Task[]; @@ -18,36 +14,22 @@ interface TasksListProps { const TasksList: React.FC = ({ tasks, isLoading, isError }) => { const { userData } = useUserProvider(); - const { selectedArtist } = useArtistProvider(); // Extract unique account IDs from tasks const accountIds = useMemo( - () => [...new Set(tasks.map(task => task.account_id))], - [tasks] + () => [...new Set(tasks.map((task) => task.account_id))], + [tasks], ); // Batch fetch emails for all task owners - const { data: accountEmails = [] } = useQuery({ - queryKey: ["task-owner-emails", accountIds], - queryFn: async () => { - if (accountIds.length === 0 || !userData) return []; - const params = new URLSearchParams(); - accountIds.forEach(id => params.append("accountIds", id)); - params.append("currentAccountId", userData.id); - if (selectedArtist) { - params.append("artistAccountId", selectedArtist.account_id); - } - const response = await fetch(`/api/account-emails?${params}`); - if (!response.ok) throw new Error("Failed to fetch emails"); - return response.json(); - }, - enabled: accountIds.length > 0 && !!userData, + const { data: accountEmails = [] } = useAccountEmails({ + accountIds, }); // Create lookup map for O(1) email access const emailByAccountId = useMemo(() => { const map = new Map(); - accountEmails.forEach(ae => { + accountEmails.forEach((ae) => { if (ae.account_id && ae.email) { map.set(ae.account_id, ae.email); } @@ -56,7 +38,11 @@ const TasksList: React.FC = ({ tasks, isLoading, isError }) => { }, [accountEmails]); if (isError) { - return
Failed to load tasks
; + return ( +
+ Failed to load tasks +
+ ); } if (isLoading || !userData) { @@ -88,8 +74,8 @@ const TasksList: React.FC = ({ tasks, isLoading, isError }) => { index !== tasks.length - 1 ? "border-b border-border " : "" } > -
diff --git a/hooks/useAccountEmails.ts b/hooks/useAccountEmails.ts new file mode 100644 index 000000000..5cb9f6c99 --- /dev/null +++ b/hooks/useAccountEmails.ts @@ -0,0 +1,43 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { usePrivy } from "@privy-io/react-auth"; +import { + fetchAccountEmails, + type AccountEmail, +} from "@/lib/accounts/fetchAccountEmails"; + +interface UseAccountEmailsParams { + accountIds: string[]; + enabled?: boolean; +} + +/** + * Fetches account email rows for one or more account IDs the authenticated user can access. + */ +export function useAccountEmails({ + accountIds, + enabled = true, +}: UseAccountEmailsParams) { + const { getAccessToken, authenticated } = usePrivy(); + + return useQuery({ + queryKey: ["account-emails", accountIds], + queryFn: async () => { + if (accountIds.length === 0) { + return []; + } + + const accessToken = await getAccessToken(); + if (!accessToken) { + throw new Error("Please sign in to view account emails"); + } + + return fetchAccountEmails({ + accessToken, + accountIds, + }); + }, + enabled: enabled && accountIds.length > 0 && authenticated, + }); +} diff --git a/lib/accounts/fetchAccountEmails.ts b/lib/accounts/fetchAccountEmails.ts new file mode 100644 index 000000000..7f1ec23ea --- /dev/null +++ b/lib/accounts/fetchAccountEmails.ts @@ -0,0 +1,40 @@ +import { getClientApiBaseUrl } from "@/lib/api/getClientApiBaseUrl"; +import { Tables } from "@/types/database.types"; + +export type AccountEmail = Tables<"account_emails">; + +interface FetchAccountEmailsParams { + accessToken: string; + accountIds: string[]; +} + +/** + * Fetches account email rows for the provided account IDs. + */ +export async function fetchAccountEmails({ + accessToken, + accountIds, +}: FetchAccountEmailsParams): Promise { + if (accountIds.length === 0) { + return []; + } + + const url = new URL(`${getClientApiBaseUrl()}/api/accounts/emails`); + accountIds.forEach((accountId) => { + url.searchParams.append("account_id", accountId); + }); + + const response = await fetch(url.toString(), { + method: "GET", + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + if (!response.ok) { + const error = await response.json().catch(() => null); + throw new Error(error?.error || "Failed to fetch account emails"); + } + + return response.json(); +}