diff --git a/src/app/api/auth/logout/route.ts b/src/app/api/auth/logout/route.ts index 8880eea3..853aa50c 100644 --- a/src/app/api/auth/logout/route.ts +++ b/src/app/api/auth/logout/route.ts @@ -27,7 +27,6 @@ export async function POST(request: NextRequest) { }; response.cookies.set('__session', '', cookieOptions); - response.cookies.set('firebase-auth-token', '', { ...cookieOptions, httpOnly: false }); return response; } diff --git a/src/app/api/auth/set-session/route.ts b/src/app/api/auth/set-session/route.ts index 12fcb2ae..2dfe61c1 100644 --- a/src/app/api/auth/set-session/route.ts +++ b/src/app/api/auth/set-session/route.ts @@ -40,7 +40,7 @@ export async function POST(request: NextRequest) { let decodedToken; try { console.log(`[set-session] calling verifyIdToken +${Date.now() - t0}ms`); - decodedToken = await withTimeout(adminAuth.verifyIdToken(idToken, true), 10000, 'verifyIdToken'); + decodedToken = await withTimeout(adminAuth.verifyIdToken(idToken, true), 25000, 'verifyIdToken'); console.log(`[set-session] verifyIdToken done +${Date.now() - t0}ms`); } catch (verifyError: any) { const isTimeout = verifyError?.message?.includes('timed out'); @@ -56,7 +56,7 @@ export async function POST(request: NextRequest) { let sessionCookie: string; try { console.log(`[set-session] calling createSessionCookie +${Date.now() - t0}ms`); - sessionCookie = await withTimeout(adminAuth.createSessionCookie(idToken, { expiresIn: SESSION_EXPIRES_MS }), 10000, 'createSessionCookie'); + sessionCookie = await withTimeout(adminAuth.createSessionCookie(idToken, { expiresIn: SESSION_EXPIRES_MS }), 25000, 'createSessionCookie'); console.log(`[set-session] createSessionCookie done +${Date.now() - t0}ms`); } catch (cookieError: any) { const isTimeout = cookieError?.message?.includes('timed out'); diff --git a/src/app/dashboard/analytics/page.tsx b/src/app/dashboard/analytics/page.tsx index 85adc014..4e22fafc 100644 --- a/src/app/dashboard/analytics/page.tsx +++ b/src/app/dashboard/analytics/page.tsx @@ -3,6 +3,7 @@ import { redirect } from 'next/navigation'; import { getAllGamesAdmin } from '@/lib/firebase/admin-services/games'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { TrendingUp, Target, Award, BarChart3, Goal, Shield, Users } from 'lucide-react'; +import { BackToDashboard } from '@/components/ui/back-to-dashboard'; // Position category mapping const POSITION_CATEGORIES: Record = { @@ -121,6 +122,8 @@ export default async function AnalyticsPage() { return (
+ + {/* Header */}

Analytics

diff --git a/src/app/dashboard/athletes/page.tsx b/src/app/dashboard/athletes/page.tsx index 92bcbc99..93288fd3 100644 --- a/src/app/dashboard/athletes/page.tsx +++ b/src/app/dashboard/athletes/page.tsx @@ -5,6 +5,7 @@ import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Plus } from 'lucide-react'; import Link from 'next/link'; +import { BackToDashboard } from '@/components/ui/back-to-dashboard'; import { calculateAge, getInitials, getAvatarColor } from '@/lib/player-utils'; import type { Player } from '@/types/firestore'; @@ -48,6 +49,8 @@ export default async function AthletesPage() { return (
+ + {/* Header Section */}
diff --git a/src/app/dashboard/games/page.tsx b/src/app/dashboard/games/page.tsx index ac051688..30eef877 100644 --- a/src/app/dashboard/games/page.tsx +++ b/src/app/dashboard/games/page.tsx @@ -2,8 +2,8 @@ import { authWithProfile } from '@/lib/auth'; import { redirect } from 'next/navigation'; import { getAllGamesAdmin } from '@/lib/firebase/admin-services/games'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { ChevronLeft } from 'lucide-react'; import Link from 'next/link'; +import { BackToDashboard } from '@/components/ui/back-to-dashboard'; import { formatGameDate, getResultBadgeClasses, @@ -42,21 +42,14 @@ export default async function GamesHistoryPage() { return (
+ + {/* PAGE HEADER */} -
-
-

Games History

-

- Complete history of all logged games across all athletes -

-
- - - Back to Dashboard - +
+

Games History

+

+ Complete history of all logged games across all athletes +

{/* SUMMARY STATS */} diff --git a/src/app/dashboard/profile/page.tsx b/src/app/dashboard/profile/page.tsx index 33cddf92..2177aa7a 100644 --- a/src/app/dashboard/profile/page.tsx +++ b/src/app/dashboard/profile/page.tsx @@ -4,6 +4,7 @@ import { getUserProfileAdmin } from '@/lib/firebase/admin-services/users'; import { getPlayersAdmin } from '@/lib/firebase/admin-services/players'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; +import { BackToDashboard } from '@/components/ui/back-to-dashboard'; export default async function ProfilePage() { // Firebase Admin auth check @@ -24,6 +25,8 @@ export default async function ProfilePage() { return (
+ +

Profile

diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx index 99fc0352..f052875e 100644 --- a/src/app/dashboard/settings/page.tsx +++ b/src/app/dashboard/settings/page.tsx @@ -5,6 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { PinSettingsForm } from './pin-settings-form'; import Link from 'next/link'; import { CreditCard } from 'lucide-react'; +import { BackToDashboard } from '@/components/ui/back-to-dashboard'; export default async function SettingsPage() { // Firebase Admin auth check @@ -22,6 +23,8 @@ export default async function SettingsPage() { return (

+ +

Settings

diff --git a/src/app/globals.css b/src/app/globals.css index a7f70f89..abb149d4 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -24,6 +24,8 @@ --input: 240 5.9% 90%; --ring: 240 5.9% 10%; --radius: 0.5rem; + --sidebar: 0 0% 98%; + --sidebar-foreground: 240 5.9% 10%; } } diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index b579ac56..1bea99da 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -47,39 +47,51 @@ function LoginContent() { 'Sign in' ); - // Step 2: Get ID token and set cookies + // Step 2: Get ID token and set server session cookie const idToken = await user.getIdToken(); - // Set client-side fallback cookie FIRST (before any network call or navigation) - // max-age=3600 matches Firebase ID token expiry (1 hour) - // Middleware reads this cookie as fallback if __session is absent - const isSecure = window.location.protocol === 'https:'; - document.cookie = `firebase-auth-token=${idToken}; path=/; max-age=3600${isSecure ? '; secure' : ''}; samesite=lax`; - - // AWAIT the server-side session cookie POST with a 15s timeout - // This sets __session (14-day, httpOnly) — the real long-term auth mechanism - // Fallback cookie above guarantees dashboard access even if this times out + // Set server-side session cookie (__session, 14-day, httpOnly). + // Uses a 30s timeout (cold starts can take 10-20s) with one automatic retry + // on transient failures (504/500/network error). console.log('[Login] Setting server session cookie...'); - try { + const setSession = async (attempt: number) => { const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 15000); - const response = await fetch('/api/auth/set-session', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ idToken }), - credentials: 'include', - signal: controller.signal, - }); - clearTimeout(timeoutId); - if (!response.ok) { - console.error('[Login] Session cookie API error:', response.status); - } else { + const timer = setTimeout(() => controller.abort(), 30000); + try { + const response = await fetch('/api/auth/set-session', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ idToken }), + credentials: 'include', + signal: controller.signal, + }); + clearTimeout(timer); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + const err = new Error(body.error || `Server returned ${response.status}`); + (err as any).status = response.status; + throw err; + } console.log('[Login] Server session cookie set successfully'); + } catch (err: any) { + clearTimeout(timer); + const isRetryable = attempt < 2 && ( + err.name === 'AbortError' || + err.status === 504 || + err.status === 500 || + err.message?.includes('fetch') + ); + if (isRetryable) { + console.warn(`[Login] Session attempt ${attempt} failed (${err.message}), retrying...`); + return setSession(attempt + 1); + } + if (err.name === 'AbortError') { + throw new Error('Session request timed out. Please check your connection and try again.'); + } + throw new Error(err.message || 'Unable to establish session. Please try again.'); } - } catch (sessionError: any) { - // POST timed out or failed — fallback cookie already set, user still gets in - console.warn('[Login] Server session cookie failed (non-fatal):', sessionError?.message); - } + }; + await setSession(1); // Step 3: Redirect to dashboard router.push('/dashboard'); diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index 93e288d8..99d13a8e 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -18,7 +18,7 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { User as FirebaseUser } from 'firebase/auth'; -import { onAuthStateChange, onIdTokenChange } from '@/lib/firebase/auth'; +import { onAuthStateChange } from '@/lib/firebase/auth'; import { isE2ETestMode } from '@/lib/e2e'; interface ProtectedRouteProps { @@ -51,24 +51,6 @@ export default function ProtectedRoute({ }; }, []); - // Keep the firebase-auth-token fallback cookie fresh by refreshing it whenever - // Firebase auto-renews the ID token (~every 55 minutes). Without this, the - // fallback cookie would expire after 1 hour if the __session POST ever failed. - useEffect(() => { - const unsubscribe = onIdTokenChange(async (firebaseUser) => { - if (firebaseUser) { - try { - const token = await firebaseUser.getIdToken(); - const isSecure = window.location.protocol === 'https:'; - document.cookie = `firebase-auth-token=${token}; path=/; max-age=3600${isSecure ? '; secure' : ''}; samesite=lax`; - } catch { - // Non-fatal: if token refresh fails, existing cookie remains until expiry - } - } - }); - return () => unsubscribe(); - }, []); - useEffect(() => { // Only handle email verification redirect - middleware handles auth if (!loading && user && requireEmailVerification && !user.emailVerified) { diff --git a/src/components/layout/app-sidebar-simple.tsx b/src/components/layout/app-sidebar-simple.tsx index bdcd36b4..ed04d4f3 100644 --- a/src/components/layout/app-sidebar-simple.tsx +++ b/src/components/layout/app-sidebar-simple.tsx @@ -74,9 +74,6 @@ export default function AppSidebarSimple() { // Clear Firebase client-side auth await firebaseSignOut(); - // Clear client-set fallback cookie - document.cookie = 'firebase-auth-token=; path=/; max-age=0'; - // Clear server-side session cookie await fetch('/api/auth/logout', { method: 'POST' }); @@ -91,14 +88,7 @@ export default function AppSidebarSimple() { return ( -

- +
H @@ -107,6 +97,13 @@ export default function AppSidebarSimple() { HUSTLE
+
diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index f0823dad..942c779a 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -17,7 +17,7 @@ export default function Header({ user }: HeaderProps) {
-

Dashboard

+

Hustle

diff --git a/src/components/layout/user-nav.tsx b/src/components/layout/user-nav.tsx index bc67b518..2d2f795a 100644 --- a/src/components/layout/user-nav.tsx +++ b/src/components/layout/user-nav.tsx @@ -38,9 +38,6 @@ export function UserNav({ user }: UserNavProps) { // Clear Firebase client-side auth await firebaseSignOut(); - // Clear client-set fallback cookie - document.cookie = 'firebase-auth-token=; path=/; max-age=0'; - // Clear server-side session cookie await fetch('/api/auth/logout', { method: 'POST' }); diff --git a/src/components/ui/back-to-dashboard.tsx b/src/components/ui/back-to-dashboard.tsx new file mode 100644 index 00000000..9d74e5b6 --- /dev/null +++ b/src/components/ui/back-to-dashboard.tsx @@ -0,0 +1,14 @@ +import Link from 'next/link'; +import { ArrowLeft } from 'lucide-react'; + +export function BackToDashboard() { + return ( + + + Back to Dashboard + + ); +} diff --git a/src/lib/firebase/auth.ts b/src/lib/firebase/auth.ts index ec09be01..edd6056c 100644 --- a/src/lib/firebase/auth.ts +++ b/src/lib/firebase/auth.ts @@ -15,7 +15,6 @@ import { updatePassword, User as FirebaseUser, onAuthStateChanged, - onIdTokenChanged, } from 'firebase/auth'; import { auth } from './config'; import { createUser, markEmailVerified } from './services/users'; @@ -167,16 +166,6 @@ export function onAuthStateChange(callback: (user: FirebaseUser | null) => void) return onAuthStateChanged(auth, callback); } -/** - * Listen to ID token changes (fires on sign-in, sign-out, and token refresh) - * - * Firebase auto-refreshes ID tokens ~5 minutes before expiry (every ~55 min). - * Use this to keep the firebase-auth-token fallback cookie fresh. - */ -export function onIdTokenChange(callback: (user: FirebaseUser | null) => void): () => void { - return onIdTokenChanged(auth, callback); -} - /** * Check if user is authenticated */ diff --git a/src/middleware.ts b/src/middleware.ts index 04b35e8b..afefa823 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -69,9 +69,7 @@ function isPublicApiRoute(pathname: string): boolean { * Get session cookie from request */ function getSessionCookie(request: NextRequest): string | null { - return request.cookies.get('__session')?.value - || request.cookies.get('firebase-auth-token')?.value - || null; + return request.cookies.get('__session')?.value || null; } /** diff --git a/tailwind.config.ts b/tailwind.config.ts index d5f79a2f..9014cb4b 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -43,6 +43,10 @@ const config: Config = { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))", }, + sidebar: { + DEFAULT: "hsl(var(--sidebar))", + foreground: "hsl(var(--sidebar-foreground))", + }, }, borderRadius: { lg: "var(--radius)",