Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/app/api/auth/logout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
4 changes: 2 additions & 2 deletions src/app/api/auth/set-session/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand Down
3 changes: 3 additions & 0 deletions src/app/dashboard/analytics/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
Expand Down Expand Up @@ -121,6 +122,8 @@ export default async function AnalyticsPage() {

return (
<div className="flex flex-col gap-6">
<BackToDashboard />

{/* Header */}
<div>
<h1 className="text-3xl font-bold text-zinc-900">Analytics</h1>
Expand Down
3 changes: 3 additions & 0 deletions src/app/dashboard/athletes/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

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

return (
<div className="flex flex-col gap-4">
<BackToDashboard />

{/* Header Section */}
<div className="flex items-start justify-between flex-wrap gap-4">
<div>
Expand Down
23 changes: 8 additions & 15 deletions src/app/dashboard/games/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -42,21 +42,14 @@ export default async function GamesHistoryPage() {

return (
<div className="space-y-6">
<BackToDashboard />

{/* PAGE HEADER */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-zinc-900">Games History</h1>
<p className="text-zinc-600 mt-2">
Complete history of all logged games across all athletes
</p>
</div>
<Link
href="/dashboard"
className="flex items-center gap-2 text-base text-zinc-700 hover:text-zinc-900 transition-colors"
>
<ChevronLeft className="h-4 w-4" />
Back to Dashboard
</Link>
<div>
<h1 className="text-3xl font-bold text-zinc-900">Games History</h1>
<p className="text-zinc-600 mt-2">
Complete history of all logged games across all athletes
</p>
</div>

{/* SUMMARY STATS */}
Expand Down
3 changes: 3 additions & 0 deletions src/app/dashboard/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,6 +25,8 @@ export default async function ProfilePage() {

return (
<div className="space-y-6">
<BackToDashboard />

<div>
<h1 className="text-3xl font-bold text-zinc-900">Profile</h1>
<p className="text-zinc-600 mt-2">
Expand Down
3 changes: 3 additions & 0 deletions src/app/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,6 +23,8 @@ export default async function SettingsPage() {

return (
<div className="space-y-6">
<BackToDashboard />

<div>
<h1 className="text-3xl font-bold text-zinc-900">Settings</h1>
<p className="text-zinc-600 mt-2">
Expand Down
2 changes: 2 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
}
}

Expand Down
66 changes: 39 additions & 27 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
20 changes: 1 addition & 19 deletions src/components/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
19 changes: 8 additions & 11 deletions src/components/layout/app-sidebar-simple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' });

Expand All @@ -91,14 +88,7 @@ export default function AppSidebarSimple() {
return (
<Sidebar className="bg-zinc-50 border-r border-zinc-200">
<SidebarHeader className='p-4 border-b border-zinc-200'>
<div className='flex items-center gap-3'>
<button
onClick={toggleSidebar}
className='w-8 h-8 bg-zinc-200 hover:bg-zinc-300 rounded-md flex items-center justify-center transition-colors'
title='Collapse Sidebar'
>
<PanelLeftClose className='h-4 w-4 text-zinc-700' />
</button>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<div className='w-8 h-8 bg-zinc-900 rounded-md flex items-center justify-center'>
<span className='text-white font-bold text-sm'>H</span>
Expand All @@ -107,6 +97,13 @@ export default function AppSidebarSimple() {
HUSTLE<sup className="text-[0.5em] align-super">™</sup>
</span>
</div>
<button
onClick={toggleSidebar}
className='w-8 h-8 bg-zinc-200 hover:bg-zinc-300 rounded-md flex items-center justify-center transition-colors'
title='Collapse Sidebar'
>
<PanelLeftClose className='h-4 w-4 text-zinc-700' />
</button>
</div>
</SidebarHeader>

Expand Down
2 changes: 1 addition & 1 deletion src/components/layout/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function Header({ user }: HeaderProps) {
<div className='flex items-center gap-2 px-4'>
<SidebarTrigger className='-ml-1' />
<Separator orientation='vertical' className='mr-2 h-4' />
<h2 className='text-lg font-semibold'>Dashboard</h2>
<h2 className='text-lg font-semibold'>Hustle</h2>
</div>

<div className='flex items-center gap-2 px-4'>
Expand Down
3 changes: 0 additions & 3 deletions src/components/layout/user-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' });

Expand Down
14 changes: 14 additions & 0 deletions src/components/ui/back-to-dashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Link from 'next/link';
import { ArrowLeft } from 'lucide-react';

export function BackToDashboard() {
return (
<Link
href="/dashboard"
className="inline-flex items-center gap-2 text-sm text-zinc-600 hover:text-zinc-900"
>
<ArrowLeft className="w-4 h-4" />
Back to Dashboard
</Link>
);
}
11 changes: 0 additions & 11 deletions src/lib/firebase/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
updatePassword,
User as FirebaseUser,
onAuthStateChanged,
onIdTokenChanged,
} from 'firebase/auth';
import { auth } from './config';
import { createUser, markEmailVerified } from './services/users';
Expand Down Expand Up @@ -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
*/
Expand Down
4 changes: 1 addition & 3 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
4 changes: 4 additions & 0 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
Loading