Skip to content

Commit 5da50e6

Browse files
committed
fix: Phase 1 re-audit — 3 regressions + 3 critical fixes
Regressions from Run 1 (broken functionality): - C2: Sync IDOR check always rejected (anon-UUID vs Google ID mismatch). Fix: tag session with authenticated user's ID instead of rejecting. - C6: Weekly reports page called 501 route (server Dexie removed). Fix: generate reports client-side using generateWeeklyReport() directly. - C9: Feed reactions broken (reactedBy stripped by toClient). Fix: restore reactedBy in response (needed for reaction highlighting). Other critical/high fixes: - C5: rAF inference loop no longer runs before camera+models ready. - C10: Feed page uses useAuth() user.id instead of duplicate session fetch. - H12: Disable X-Powered-By header (poweredByHeader: false). - C13: Playwright retries:1 in CI (traces now captured on failure).
1 parent 48b9699 commit 5da50e6

7 files changed

Lines changed: 35 additions & 27 deletions

File tree

app/api/feed/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ function toClient(post: FeedPostItem) {
276276
content: post.content,
277277
category: post.category,
278278
reactions: post.reactions,
279-
// Only expose reaction counts, not who reacted (privacy)
279+
reactedBy: post.reactedBy || { heart: [], helpful: [], relate: [] },
280280
createdAt: post.createdAt,
281281
anonymous: post.anonymous,
282282
};

app/api/sync/route.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,11 @@ export async function POST(req: NextRequest) {
5959

6060
const { session, biomarkers } = body;
6161

62-
// IDOR check — user can only sync their own data
63-
if (session.userId !== authResult.id) {
64-
return NextResponse.json({ error: "userId mismatch" }, { status: 403 });
65-
}
62+
// IDOR check — user can only sync their own data.
63+
// Sessions may use an anonymous "anon-*" localStorage ID or the Google OAuth ID.
64+
// Both are valid — the auth gate already ensures the request is from an authenticated user.
65+
// We tag the session with the authenticated user's ID for attribution.
66+
session.userId = authResult.id;
6667

6768
const sessionsTable = process.env.DYNAMODB_SESSIONS_TABLE;
6869
const biomarkersTable = process.env.DYNAMODB_BIOMARKERS_TABLE;

app/feed/page.tsx

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,27 +47,18 @@ const REACTION_CONFIG = [
4747

4848
export default function FeedPage() {
4949
const { theme, toggle: toggleTheme } = useTheme();
50-
const { loading: authLoading, isAuthenticated } = useAuthGuard();
50+
const { loading: authLoading, isAuthenticated, user } = useAuthGuard();
5151
const [posts, setPosts] = useState<FeedPost[]>([]);
5252
const [filter, setFilter] = useState<Category>("all");
5353
const [content, setContent] = useState("");
5454
const [category, setCategory] = useState<FeedPost["category"]>("tip");
5555
const [posting, setPosting] = useState(false);
5656
const [loading, setLoading] = useState(true);
57-
const [userId, setUserId] = useState("");
5857
const [showCompose, setShowCompose] = useState(false);
5958
const [anonymous, setAnonymous] = useState(true);
6059

61-
// Get current user ID from session
62-
useEffect(() => {
63-
if (!isAuthenticated) return;
64-
fetch("/api/auth/session")
65-
.then((r) => r.json())
66-
.then((data) => {
67-
if (data.user?.id) setUserId(data.user.id);
68-
})
69-
.catch(() => {});
70-
}, [isAuthenticated]);
60+
// Use user ID from auth context (no duplicate fetch)
61+
const userId = user?.id || "";
7162

7263
const loadPosts = useCallback(async () => {
7364
try {

app/hooks/useDetectorInference.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,11 @@ export function useDetectorInference(
143143
}, [videoRef, canvasRef, isCamReady, isModelLoaded]);
144144

145145
useEffect(() => {
146+
// Only start the rAF loop when camera + models are both ready
147+
if (!isCamReady || !isModelLoaded) return;
146148
rafRef.current = requestAnimationFrame(sendFrame);
147149
return () => cancelAnimationFrame(rafRef.current);
148-
}, [sendFrame]);
150+
}, [sendFrame, isCamReady, isModelLoaded]);
149151

150152
const resetPipeline = useCallback(() => {
151153
workerRef.current?.postMessage({ type: "reset" });

app/kid-dashboard/reports/page.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import DOMPurify from "dompurify";
66
import { useAuthGuard } from "../../hooks/useAuthGuard";
77
import type { WeeklyReport } from "../../types/gameActivity";
88
import { db } from "../../lib/db/schema";
9+
import { generateWeeklyReport, renderKidReport, renderParentReport } from "../../lib/reports/weeklyReport";
910
import NavLogo from "../../components/NavLogo";
1011
import ThemeToggle from "../../components/ThemeToggle";
1112
import { useTheme } from "../../hooks/useTheme";
@@ -52,16 +53,28 @@ export default function ReportsPage() {
5253
const generateReport = async () => {
5354
setGenerating(true);
5455
try {
55-
const res = await fetch("/api/report/weekly", {
56-
method: "POST",
57-
headers: { "Content-Type": "application/json" },
58-
body: JSON.stringify({ childId, childName }),
56+
// Generate client-side (weekly reports use IndexedDB data, not server API)
57+
const data = await generateWeeklyReport(childId);
58+
const name = childName || "Superstar";
59+
const kidHtml = renderKidReport(data, name);
60+
const parentHtml = renderParentReport(data, name);
61+
const combinedHtml = `<!-- KID -->${kidHtml}<!-- SPLIT --><!-- PARENT -->${parentHtml}`;
62+
63+
await db.weeklyReports.add({
64+
childId: data.childId,
65+
weekStart: data.weekStart,
66+
weekEnd: data.weekEnd,
67+
gamesPlayed: data.gamesPlayed,
68+
avgScore: data.avgScore,
69+
topGame: data.topGame,
70+
streakDays: data.streakDays,
71+
reportHtml: combinedHtml,
72+
generatedAt: Date.now(),
73+
emailed: false,
5974
});
60-
if (res.ok) {
61-
await loadReports();
62-
}
75+
await loadReports();
6376
} catch {
64-
// silently fail
77+
// IndexedDB or generation error — silently fail
6578
} finally {
6679
setGenerating(false);
6780
}

next.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { NextConfig } from "next";
22

33
const nextConfig: NextConfig = {
4+
poweredByHeader: false,
45
images: {
56
remotePatterns: [
67
{ protocol: "https", hostname: "lh3.googleusercontent.com" },

playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { defineConfig, devices } from "@playwright/test";
33
export default defineConfig({
44
testDir: "./tests",
55
timeout: 30_000,
6-
retries: 0,
6+
retries: process.env.CI ? 1 : 0,
77
use: {
88
baseURL: "http://localhost:3003",
99
headless: true,

0 commit comments

Comments
 (0)