Skip to content

Commit db7cc56

Browse files
Miriaddashboard
andcommitted
fix: address PR review — add auth to API routes + polish
- Add Supabase session validation to /api/dashboard/metrics and /activity - Extract shared types (DashboardMetrics, ActivityItem, POLL_INTERVAL_MS) - Add 1-second tick for live 'Updated X ago' counter - Add loading spinner for RecentActivity initial fetch - Return 500 (not 200) on activity fetch error - Remove duplicate interface definitions Co-authored-by: dashboard <dashboard@miriad.systems>
1 parent 20bdd0d commit db7cc56

File tree

5 files changed

+99
-69
lines changed

5 files changed

+99
-69
lines changed
Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
import { NextResponse } from "next/server";
2+
import { createClient } from "@/lib/supabase/server";
23
import { dashboardQuery } from "@/lib/sanity/dashboard";
4+
import type { ActivityItem } from "@/lib/types/dashboard";
35

46
export const dynamic = "force-dynamic";
57

6-
interface ActivityItem {
7-
_id: string;
8-
_type: string;
9-
_updatedAt: string;
10-
title?: string;
11-
companyName?: string;
12-
status?: string;
13-
}
14-
158
export async function GET() {
9+
const hasSupabase =
10+
process.env.NEXT_PUBLIC_SUPABASE_URL &&
11+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
12+
13+
if (hasSupabase) {
14+
const supabase = await createClient();
15+
const {
16+
data: { user },
17+
} = await supabase.auth.getUser();
18+
if (!user) {
19+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
20+
}
21+
}
22+
1623
try {
1724
const items = await dashboardQuery<ActivityItem[]>(`
1825
*[_type in ["contentIdea", "automatedVideo", "sponsorLead"]] | order(_updatedAt desc) [0..9] {
@@ -28,6 +35,6 @@ export async function GET() {
2835
return NextResponse.json(items ?? []);
2936
} catch (error) {
3037
console.error("Failed to fetch activity:", error);
31-
return NextResponse.json([]);
38+
return NextResponse.json([], { status: 500 });
3239
}
3340
}

app/api/dashboard/metrics/route.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
import { NextResponse } from "next/server";
2+
import { createClient } from "@/lib/supabase/server";
23
import { dashboardQuery } from "@/lib/sanity/dashboard";
4+
import type { DashboardMetrics } from "@/lib/types/dashboard";
35

46
export const dynamic = "force-dynamic";
57

68
export async function GET() {
9+
const hasSupabase =
10+
process.env.NEXT_PUBLIC_SUPABASE_URL &&
11+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
12+
13+
if (hasSupabase) {
14+
const supabase = await createClient();
15+
const {
16+
data: { user },
17+
} = await supabase.auth.getUser();
18+
if (!user) {
19+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
20+
}
21+
}
22+
723
try {
824
const [videosPublished, flaggedVideos, newIdeas, sponsorPipeline] =
925
await Promise.all([
@@ -21,12 +37,14 @@ export async function GET() {
2137
),
2238
]);
2339

24-
return NextResponse.json({
40+
const metrics: DashboardMetrics = {
2541
videosPublished: videosPublished ?? 0,
2642
flaggedForReview: (flaggedVideos ?? 0) + (newIdeas ?? 0),
2743
sponsorPipeline: sponsorPipeline ?? 0,
2844
revenue: null,
29-
});
45+
};
46+
47+
return NextResponse.json(metrics);
3048
} catch (error) {
3149
console.error("Failed to fetch dashboard metrics:", error);
3250
return NextResponse.json(

components/recent-activity.tsx

Lines changed: 21 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,8 @@
22

33
import { useEffect, useState, useCallback } from "react";
44
import { Badge } from "@/components/ui/badge";
5-
import { Lightbulb, FileVideo, Handshake, Clock } from "lucide-react";
6-
7-
interface ActivityItem {
8-
_id: string;
9-
_type: string;
10-
_updatedAt: string;
11-
title?: string;
12-
companyName?: string;
13-
status?: string;
14-
}
5+
import { Lightbulb, FileVideo, Handshake, Clock, Loader2 } from "lucide-react";
6+
import { POLL_INTERVAL_MS, type ActivityItem } from "@/lib/types/dashboard";
157

168
const typeConfig: Record<
179
string,
@@ -20,20 +12,17 @@ const typeConfig: Record<
2012
contentIdea: {
2113
icon: Lightbulb,
2214
label: "Idea",
23-
color:
24-
"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
15+
color: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300",
2516
},
2617
automatedVideo: {
2718
icon: FileVideo,
2819
label: "Video",
29-
color:
30-
"bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300",
20+
color: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300",
3121
},
3222
sponsorLead: {
3323
icon: Handshake,
3424
label: "Sponsor",
35-
color:
36-
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
25+
color: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
3726
},
3827
};
3928

@@ -49,6 +38,7 @@ function formatTimeAgo(dateString: string) {
4938

5039
export function RecentActivity() {
5140
const [items, setItems] = useState<ActivityItem[]>([]);
41+
const [isLoading, setIsLoading] = useState(true);
5242

5343
const fetchActivity = useCallback(async () => {
5444
try {
@@ -59,52 +49,45 @@ export function RecentActivity() {
5949
}
6050
} catch (error) {
6151
console.error("Failed to fetch activity:", error);
52+
} finally {
53+
setIsLoading(false);
6254
}
6355
}, []);
6456

6557
useEffect(() => {
6658
fetchActivity();
67-
const interval = setInterval(fetchActivity, 30000);
59+
const interval = setInterval(fetchActivity, POLL_INTERVAL_MS);
6860
return () => clearInterval(interval);
6961
}, [fetchActivity]);
7062

7163
return (
7264
<div className="rounded-lg border p-6">
7365
<h2 className="text-lg font-semibold">Recent Activity</h2>
74-
{items.length === 0 ? (
66+
{isLoading ? (
67+
<div className="mt-4 flex items-center justify-center py-8">
68+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
69+
</div>
70+
) : items.length === 0 ? (
7571
<p className="mt-4 text-sm text-muted-foreground">
76-
No activity yet \u2014 content will appear here as the pipeline
77-
runs.
72+
No activity yet \u2014 content will appear here as the pipeline runs.
7873
</p>
7974
) : (
8075
<div className="mt-4 space-y-3">
8176
{items.map((item) => {
82-
const config =
83-
typeConfig[item._type] ?? typeConfig.contentIdea;
77+
const config = typeConfig[item._type] ?? typeConfig.contentIdea;
8478
const Icon = config.icon;
85-
const name =
86-
item.title || item.companyName || "Untitled";
79+
const name = item.title || item.companyName || "Untitled";
8780
return (
88-
<div
89-
key={item._id}
90-
className="flex items-center gap-3"
91-
>
81+
<div key={item._id} className="flex items-center gap-3">
9282
<Icon className="h-4 w-4 shrink-0 text-muted-foreground" />
9383
<div className="min-w-0 flex-1">
94-
<p className="truncate text-sm font-medium">
95-
{name}
96-
</p>
84+
<p className="truncate text-sm font-medium">{name}</p>
9785
<div className="flex items-center gap-2">
98-
<Badge
99-
variant="outline"
100-
className={`text-xs ${config.color}`}
101-
>
86+
<Badge variant="outline" className={`text-xs ${config.color}`}>
10287
{config.label}
10388
</Badge>
10489
{item.status && (
105-
<span className="text-xs text-muted-foreground">
106-
{item.status}
107-
</span>
90+
<span className="text-xs text-muted-foreground">{item.status}</span>
10891
)}
10992
</div>
11093
</div>

components/section-cards-live.tsx

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,14 @@ import {
1010
} from "@/components/ui/card";
1111
import { FileVideo, Flag, Handshake, DollarSign, RefreshCw } from "lucide-react";
1212
import { Button } from "@/components/ui/button";
13+
import { POLL_INTERVAL_MS, type DashboardMetrics } from "@/lib/types/dashboard";
1314

14-
interface Metrics {
15-
videosPublished: number;
16-
flaggedForReview: number;
17-
sponsorPipeline: number;
18-
revenue: number | null;
19-
}
20-
21-
export function SectionCardsLive({ initialMetrics }: { initialMetrics?: Metrics }) {
22-
const [metrics, setMetrics] = useState<Metrics>(
15+
export function SectionCardsLive({
16+
initialMetrics,
17+
}: {
18+
initialMetrics?: DashboardMetrics;
19+
}) {
20+
const [metrics, setMetrics] = useState<DashboardMetrics>(
2321
initialMetrics ?? {
2422
videosPublished: 0,
2523
flaggedForReview: 0,
@@ -29,6 +27,7 @@ export function SectionCardsLive({ initialMetrics }: { initialMetrics?: Metrics
2927
);
3028
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
3129
const [isRefreshing, setIsRefreshing] = useState(false);
30+
const [secondsAgo, setSecondsAgo] = useState(0);
3231

3332
const fetchMetrics = useCallback(async () => {
3433
try {
@@ -38,6 +37,7 @@ export function SectionCardsLive({ initialMetrics }: { initialMetrics?: Metrics
3837
const data = await res.json();
3938
setMetrics(data);
4039
setLastUpdated(new Date());
40+
setSecondsAgo(0);
4141
}
4242
} catch (error) {
4343
console.error("Failed to refresh metrics:", error);
@@ -48,10 +48,24 @@ export function SectionCardsLive({ initialMetrics }: { initialMetrics?: Metrics
4848

4949
useEffect(() => {
5050
fetchMetrics();
51-
const interval = setInterval(fetchMetrics, 30000);
51+
const interval = setInterval(fetchMetrics, POLL_INTERVAL_MS);
5252
return () => clearInterval(interval);
5353
}, [fetchMetrics]);
5454

55+
useEffect(() => {
56+
const tick = setInterval(() => {
57+
setSecondsAgo(Math.floor((Date.now() - lastUpdated.getTime()) / 1000));
58+
}, 1000);
59+
return () => clearInterval(tick);
60+
}, [lastUpdated]);
61+
62+
const timeAgo =
63+
secondsAgo < 5
64+
? "just now"
65+
: secondsAgo < 60
66+
? `${secondsAgo}s ago`
67+
: `${Math.floor(secondsAgo / 60)}m ago`;
68+
5569
const cards = [
5670
{
5771
title: "Videos Published",
@@ -79,15 +93,6 @@ export function SectionCardsLive({ initialMetrics }: { initialMetrics?: Metrics
7993
},
8094
];
8195

82-
// Format relative time
83-
const secondsAgo = Math.floor((Date.now() - lastUpdated.getTime()) / 1000);
84-
const timeAgo =
85-
secondsAgo < 5
86-
? "just now"
87-
: secondsAgo < 60
88-
? `${secondsAgo}s ago`
89-
: `${Math.floor(secondsAgo / 60)}m ago`;
90-
9196
return (
9297
<div className="flex flex-col gap-2">
9398
<div className="flex items-center justify-between">

lib/types/dashboard.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export const POLL_INTERVAL_MS = 30000;
2+
3+
export interface DashboardMetrics {
4+
videosPublished: number;
5+
flaggedForReview: number;
6+
sponsorPipeline: number;
7+
revenue: number | null;
8+
}
9+
10+
export interface ActivityItem {
11+
_id: string;
12+
_type: string;
13+
_updatedAt: string;
14+
title?: string;
15+
companyName?: string;
16+
status?: string;
17+
}

0 commit comments

Comments
 (0)