Skip to content

Commit 486b823

Browse files
opeyemiariyo-netizenclaudejeremylongshore
authored
Fix/remove otel deadlock (#39)
* fix(auth): remove broken fallback cookie and AbortController timeout Remove the firebase-auth-token fallback cookie that was never read by middleware or auth.ts, causing a false sense of resilience. Remove the AbortController timeout on the session POST that was aborting before the server could respond on cold starts. Login now awaits the session POST fully and surfaces clear error messages on failure. - Remove firebase-auth-token cookie from login, logout, sidebar, user-nav - Remove AbortController from login page session POST - Remove onIdTokenChanged listener from ProtectedRoute - Remove onIdTokenChange export from client auth module - Revert middleware to only check __session cookie Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(ui): mobile sidebar background, back buttons, header text, close btn - Fix transparent mobile sidebar by adding --sidebar CSS variable and Tailwind color mapping so bg-sidebar resolves correctly - Add "← Back to Dashboard" link to athletes, analytics, settings, profile, and games pages (matching dream-gym style) - Change header text from "Dashboard" to "Hustle" - Move sidebar close button to right of HUSTLE logo Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(auth,ui): add session POST timeout+retry, extract BackToDashboard component Address code review feedback: 1. Login session POST now has a 30s client-side timeout (AbortController) with one automatic retry on transient failures (504, 500, network error, timeout). Prevents indefinite loading state while tolerating cold starts. 2. Extract BackToDashboard into a reusable component used by athletes, analytics, games, profile, and settings pages — reduces duplication. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(auth): increase set-session timeouts from 10s to 25s for cold starts __session is now the sole auth mechanism (no fallback cookie). Server-side verifyIdToken/createSessionCookie timeouts of 10s are too aggressive for Firebase Admin cold starts (10-20s documented). Increased to 25s to stay under the client's 30s AbortController timeout while accommodating cold starts. Addresses Qodo review finding on PR #39. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: jeremylongshore <[email protected]>
1 parent 43f0674 commit 486b823

17 files changed

Lines changed: 92 additions & 93 deletions

File tree

src/app/api/auth/logout/route.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ export async function POST(request: NextRequest) {
2727
};
2828

2929
response.cookies.set('__session', '', cookieOptions);
30-
response.cookies.set('firebase-auth-token', '', { ...cookieOptions, httpOnly: false });
3130

3231
return response;
3332
}

src/app/api/auth/set-session/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export async function POST(request: NextRequest) {
4040
let decodedToken;
4141
try {
4242
console.log(`[set-session] calling verifyIdToken +${Date.now() - t0}ms`);
43-
decodedToken = await withTimeout(adminAuth.verifyIdToken(idToken, true), 10000, 'verifyIdToken');
43+
decodedToken = await withTimeout(adminAuth.verifyIdToken(idToken, true), 25000, 'verifyIdToken');
4444
console.log(`[set-session] verifyIdToken done +${Date.now() - t0}ms`);
4545
} catch (verifyError: any) {
4646
const isTimeout = verifyError?.message?.includes('timed out');
@@ -56,7 +56,7 @@ export async function POST(request: NextRequest) {
5656
let sessionCookie: string;
5757
try {
5858
console.log(`[set-session] calling createSessionCookie +${Date.now() - t0}ms`);
59-
sessionCookie = await withTimeout(adminAuth.createSessionCookie(idToken, { expiresIn: SESSION_EXPIRES_MS }), 10000, 'createSessionCookie');
59+
sessionCookie = await withTimeout(adminAuth.createSessionCookie(idToken, { expiresIn: SESSION_EXPIRES_MS }), 25000, 'createSessionCookie');
6060
console.log(`[set-session] createSessionCookie done +${Date.now() - t0}ms`);
6161
} catch (cookieError: any) {
6262
const isTimeout = cookieError?.message?.includes('timed out');

src/app/dashboard/analytics/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { redirect } from 'next/navigation';
33
import { getAllGamesAdmin } from '@/lib/firebase/admin-services/games';
44
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
55
import { TrendingUp, Target, Award, BarChart3, Goal, Shield, Users } from 'lucide-react';
6+
import { BackToDashboard } from '@/components/ui/back-to-dashboard';
67

78
// Position category mapping
89
const POSITION_CATEGORIES: Record<string, string> = {
@@ -121,6 +122,8 @@ export default async function AnalyticsPage() {
121122

122123
return (
123124
<div className="flex flex-col gap-6">
125+
<BackToDashboard />
126+
124127
{/* Header */}
125128
<div>
126129
<h1 className="text-3xl font-bold text-zinc-900">Analytics</h1>

src/app/dashboard/athletes/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Card, CardContent } from '@/components/ui/card';
55
import { Button } from '@/components/ui/button';
66
import { Plus } from 'lucide-react';
77
import Link from 'next/link';
8+
import { BackToDashboard } from '@/components/ui/back-to-dashboard';
89
import { calculateAge, getInitials, getAvatarColor } from '@/lib/player-utils';
910
import type { Player } from '@/types/firestore';
1011

@@ -48,6 +49,8 @@ export default async function AthletesPage() {
4849

4950
return (
5051
<div className="flex flex-col gap-4">
52+
<BackToDashboard />
53+
5154
{/* Header Section */}
5255
<div className="flex items-start justify-between flex-wrap gap-4">
5356
<div>

src/app/dashboard/games/page.tsx

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { authWithProfile } from '@/lib/auth';
22
import { redirect } from 'next/navigation';
33
import { getAllGamesAdmin } from '@/lib/firebase/admin-services/games';
44
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5-
import { ChevronLeft } from 'lucide-react';
65
import Link from 'next/link';
6+
import { BackToDashboard } from '@/components/ui/back-to-dashboard';
77
import {
88
formatGameDate,
99
getResultBadgeClasses,
@@ -42,21 +42,14 @@ export default async function GamesHistoryPage() {
4242

4343
return (
4444
<div className="space-y-6">
45+
<BackToDashboard />
46+
4547
{/* PAGE HEADER */}
46-
<div className="flex items-center justify-between">
47-
<div>
48-
<h1 className="text-3xl font-bold text-zinc-900">Games History</h1>
49-
<p className="text-zinc-600 mt-2">
50-
Complete history of all logged games across all athletes
51-
</p>
52-
</div>
53-
<Link
54-
href="/dashboard"
55-
className="flex items-center gap-2 text-base text-zinc-700 hover:text-zinc-900 transition-colors"
56-
>
57-
<ChevronLeft className="h-4 w-4" />
58-
Back to Dashboard
59-
</Link>
48+
<div>
49+
<h1 className="text-3xl font-bold text-zinc-900">Games History</h1>
50+
<p className="text-zinc-600 mt-2">
51+
Complete history of all logged games across all athletes
52+
</p>
6053
</div>
6154

6255
{/* SUMMARY STATS */}

src/app/dashboard/profile/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getUserProfileAdmin } from '@/lib/firebase/admin-services/users';
44
import { getPlayersAdmin } from '@/lib/firebase/admin-services/players';
55
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
66
import { Badge } from '@/components/ui/badge';
7+
import { BackToDashboard } from '@/components/ui/back-to-dashboard';
78

89
export default async function ProfilePage() {
910
// Firebase Admin auth check
@@ -24,6 +25,8 @@ export default async function ProfilePage() {
2425

2526
return (
2627
<div className="space-y-6">
28+
<BackToDashboard />
29+
2730
<div>
2831
<h1 className="text-3xl font-bold text-zinc-900">Profile</h1>
2932
<p className="text-zinc-600 mt-2">

src/app/dashboard/settings/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
55
import { PinSettingsForm } from './pin-settings-form';
66
import Link from 'next/link';
77
import { CreditCard } from 'lucide-react';
8+
import { BackToDashboard } from '@/components/ui/back-to-dashboard';
89

910
export default async function SettingsPage() {
1011
// Firebase Admin auth check
@@ -22,6 +23,8 @@ export default async function SettingsPage() {
2223

2324
return (
2425
<div className="space-y-6">
26+
<BackToDashboard />
27+
2528
<div>
2629
<h1 className="text-3xl font-bold text-zinc-900">Settings</h1>
2730
<p className="text-zinc-600 mt-2">

src/app/globals.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
--input: 240 5.9% 90%;
2525
--ring: 240 5.9% 10%;
2626
--radius: 0.5rem;
27+
--sidebar: 0 0% 98%;
28+
--sidebar-foreground: 240 5.9% 10%;
2729
}
2830
}
2931

src/app/login/page.tsx

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -47,39 +47,51 @@ function LoginContent() {
4747
'Sign in'
4848
);
4949

50-
// Step 2: Get ID token and set cookies
50+
// Step 2: Get ID token and set server session cookie
5151
const idToken = await user.getIdToken();
5252

53-
// Set client-side fallback cookie FIRST (before any network call or navigation)
54-
// max-age=3600 matches Firebase ID token expiry (1 hour)
55-
// Middleware reads this cookie as fallback if __session is absent
56-
const isSecure = window.location.protocol === 'https:';
57-
document.cookie = `firebase-auth-token=${idToken}; path=/; max-age=3600${isSecure ? '; secure' : ''}; samesite=lax`;
58-
59-
// AWAIT the server-side session cookie POST with a 15s timeout
60-
// This sets __session (14-day, httpOnly) — the real long-term auth mechanism
61-
// Fallback cookie above guarantees dashboard access even if this times out
53+
// Set server-side session cookie (__session, 14-day, httpOnly).
54+
// Uses a 30s timeout (cold starts can take 10-20s) with one automatic retry
55+
// on transient failures (504/500/network error).
6256
console.log('[Login] Setting server session cookie...');
63-
try {
57+
const setSession = async (attempt: number) => {
6458
const controller = new AbortController();
65-
const timeoutId = setTimeout(() => controller.abort(), 15000);
66-
const response = await fetch('/api/auth/set-session', {
67-
method: 'POST',
68-
headers: { 'Content-Type': 'application/json' },
69-
body: JSON.stringify({ idToken }),
70-
credentials: 'include',
71-
signal: controller.signal,
72-
});
73-
clearTimeout(timeoutId);
74-
if (!response.ok) {
75-
console.error('[Login] Session cookie API error:', response.status);
76-
} else {
59+
const timer = setTimeout(() => controller.abort(), 30000);
60+
try {
61+
const response = await fetch('/api/auth/set-session', {
62+
method: 'POST',
63+
headers: { 'Content-Type': 'application/json' },
64+
body: JSON.stringify({ idToken }),
65+
credentials: 'include',
66+
signal: controller.signal,
67+
});
68+
clearTimeout(timer);
69+
if (!response.ok) {
70+
const body = await response.json().catch(() => ({}));
71+
const err = new Error(body.error || `Server returned ${response.status}`);
72+
(err as any).status = response.status;
73+
throw err;
74+
}
7775
console.log('[Login] Server session cookie set successfully');
76+
} catch (err: any) {
77+
clearTimeout(timer);
78+
const isRetryable = attempt < 2 && (
79+
err.name === 'AbortError' ||
80+
err.status === 504 ||
81+
err.status === 500 ||
82+
err.message?.includes('fetch')
83+
);
84+
if (isRetryable) {
85+
console.warn(`[Login] Session attempt ${attempt} failed (${err.message}), retrying...`);
86+
return setSession(attempt + 1);
87+
}
88+
if (err.name === 'AbortError') {
89+
throw new Error('Session request timed out. Please check your connection and try again.');
90+
}
91+
throw new Error(err.message || 'Unable to establish session. Please try again.');
7892
}
79-
} catch (sessionError: any) {
80-
// POST timed out or failed — fallback cookie already set, user still gets in
81-
console.warn('[Login] Server session cookie failed (non-fatal):', sessionError?.message);
82-
}
93+
};
94+
await setSession(1);
8395

8496
// Step 3: Redirect to dashboard
8597
router.push('/dashboard');

src/components/ProtectedRoute.tsx

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import { useEffect, useState } from 'react';
1919
import { useRouter } from 'next/navigation';
2020
import { User as FirebaseUser } from 'firebase/auth';
21-
import { onAuthStateChange, onIdTokenChange } from '@/lib/firebase/auth';
21+
import { onAuthStateChange } from '@/lib/firebase/auth';
2222
import { isE2ETestMode } from '@/lib/e2e';
2323

2424
interface ProtectedRouteProps {
@@ -51,24 +51,6 @@ export default function ProtectedRoute({
5151
};
5252
}, []);
5353

54-
// Keep the firebase-auth-token fallback cookie fresh by refreshing it whenever
55-
// Firebase auto-renews the ID token (~every 55 minutes). Without this, the
56-
// fallback cookie would expire after 1 hour if the __session POST ever failed.
57-
useEffect(() => {
58-
const unsubscribe = onIdTokenChange(async (firebaseUser) => {
59-
if (firebaseUser) {
60-
try {
61-
const token = await firebaseUser.getIdToken();
62-
const isSecure = window.location.protocol === 'https:';
63-
document.cookie = `firebase-auth-token=${token}; path=/; max-age=3600${isSecure ? '; secure' : ''}; samesite=lax`;
64-
} catch {
65-
// Non-fatal: if token refresh fails, existing cookie remains until expiry
66-
}
67-
}
68-
});
69-
return () => unsubscribe();
70-
}, []);
71-
7254
useEffect(() => {
7355
// Only handle email verification redirect - middleware handles auth
7456
if (!loading && user && requireEmailVerification && !user.emailVerified) {

0 commit comments

Comments
 (0)