diff --git a/public/sw.js b/public/sw.js index ade69d1..3c2b889 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,8 +1,8 @@ // CompetenceTrack — Service Worker for PWA Offline Support -const CACHE_NAME = 'competencetrack-v2'; -const STATIC_CACHE = 'competencetrack-static-v2'; -const DATA_CACHE = 'competencetrack-data-v2'; -const OFFLINE_CACHE = 'competencetrack-offline-v2'; +const CACHE_NAME = 'competencetrack-v3'; +const STATIC_CACHE = 'competencetrack-static-v3'; +const DATA_CACHE = 'competencetrack-data-v3'; +const OFFLINE_CACHE = 'competencetrack-offline-v3'; // Static assets to cache on install const STATIC_ASSETS = [ @@ -80,13 +80,20 @@ self.addEventListener('fetch', (event) => { return; } - // Static assets (CSS, JS, images) — CacheFirst strategy - // But use NetworkFirst for JS/CSS from Next.js dev server (_next/) to avoid stale module factories + // Never cache /_next/ paths — Turbopack recompiles frequently and cached chunks + // cause "module factory is not available" errors. Always fetch from network. + if (url.pathname.startsWith('/_next/')) { + event.respondWith( + fetch(request).catch(() => new Response('Offline', { status: 503 })) + ); + return; + } + + // Static assets (CSS, JS, images) from other origins — CacheFirst strategy if ( url.pathname.endsWith('.css') || url.pathname.endsWith('.js') ) { - // Always use NetworkFirst for JS/CSS to avoid stale Turbopack chunks event.respondWith(networkFirstStrategy(request, STATIC_CACHE)); return; } diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts index 4fa7708..7bd6baa 100644 --- a/src/app/api/auth/route.ts +++ b/src/app/api/auth/route.ts @@ -7,6 +7,9 @@ import { createSession, getSession, clearSession, + isSecureRequest, + SESSION_COOKIE_NAME, + getSessionCookieOptions, } from '@/lib/auth'; import { withRateLimit, checkRateLimit, getRateLimitKey, getRateLimitHeaders, createRateLimitResponse } from '@/lib/rate-limit'; @@ -60,9 +63,13 @@ export const POST = withRateLimit(async function POST(request: Request) { ); } - await createSession(user.id); + const secure = isSecureRequest(request); + await createSession(user.id, secure); - return NextResponse.json({ + // Also set cookie on the response object to guarantee the Set-Cookie + // header is included. In some Next.js App Router scenarios, cookies().set() + // alone may not propagate to the NextResponse body. + const response = NextResponse.json({ id: user.id, email: user.email, firstName: user.firstName, @@ -72,6 +79,8 @@ export const POST = withRateLimit(async function POST(request: Request) { locale: user.locale, isDemo: user.isDemo, }); + response.cookies.set(SESSION_COOKIE_NAME, user.id, getSessionCookieOptions(secure)); + return response; } // ── Register ── @@ -108,9 +117,10 @@ export const POST = withRateLimit(async function POST(request: Request) { }, }); - await createSession(user.id); + const secure = isSecureRequest(request); + await createSession(user.id, secure); - return NextResponse.json( + const response = NextResponse.json( { id: user.id, email: user.email, @@ -123,28 +133,56 @@ export const POST = withRateLimit(async function POST(request: Request) { }, { status: 201 } ); + response.cookies.set(SESSION_COOKIE_NAME, user.id, getSessionCookieOptions(secure)); + return response; } // ── Logout ── if (action === 'logout') { await clearSession(); - return NextResponse.json({ message: 'Logged out' }); + + const response = NextResponse.json({ message: 'Logged out' }); + response.cookies.delete(SESSION_COOKIE_NAME); + return response; } return NextResponse.json({ error: 'Unknown action' }, { status: 400 }); } catch (error) { - console.error('Auth error:', error); + console.error('Auth POST error:', error); + + // Return more specific error info for debugging + const message = + error instanceof Error ? error.message : 'Unknown error'; + const isZod = error instanceof z.ZodError; + const isPrisma = + error instanceof Error && + (error.message.includes('Prisma') || error.constructor.name.includes('Prisma')); + + if (isZod) { + return NextResponse.json( + { error: 'Validation error', details: (error as z.ZodError).issues }, + { status: 400 } + ); + } + + if (isPrisma) { + return NextResponse.json( + { error: 'Database error', detail: message }, + { status: 500 } + ); + } + return NextResponse.json( - { error: 'Internal server error' }, + { error: 'Internal server error', detail: message }, { status: 500 } ); } }, 'auth'); export async function GET(request: Request) { - // Apply rate limit for auth check endpoint + // Apply rate limit for auth check endpoint — 120 per minute const key = getRateLimitKey(request); - const result = checkRateLimit(key, 60, 60 * 1000); // 60 per minute for GET + const result = checkRateLimit(key, 120, 60 * 1000); if (!result.allowed) { const rateLimitResponse = createRateLimitResponse(result); @@ -167,9 +205,11 @@ export async function GET(request: Request) { } return response; } catch (error) { - console.error('Auth me error:', error); + console.error('Auth GET error:', error); + const message = + error instanceof Error ? error.message : 'Unknown error'; return NextResponse.json( - { error: 'Internal server error' }, + { error: 'Internal server error', detail: message }, { status: 500 } ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 260cc57..a080b2e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { useAppStore } from '@/lib/store'; import { fetchCurrentUser } from '@/lib/api'; import AuthView from '@/components/auth-view'; @@ -10,8 +10,13 @@ export default function Home() { const currentUser = useAppStore((s) => s.currentUser); const isLoadingAuth = useAppStore((s) => s.isLoadingAuth); const setCurrentUser = useAppStore((s) => s.setCurrentUser); + const hasCheckedRef = useRef(false); useEffect(() => { + // Prevent duplicate auth checks: only run once per component lifecycle + if (hasCheckedRef.current) return; + hasCheckedRef.current = true; + async function checkAuth() { try { const user = await fetchCurrentUser(); diff --git a/src/components/offline-indicator.tsx b/src/components/offline-indicator.tsx index 877c681..72c31cf 100644 --- a/src/components/offline-indicator.tsx +++ b/src/components/offline-indicator.tsx @@ -179,6 +179,9 @@ export function useServiceWorker() { useEffect(() => { if (typeof window === 'undefined') return; if (!('serviceWorker' in navigator)) return; + // Skip service worker registration during development to avoid stale + // Turbopack chunk caching ("module factory is not available" errors) + if (process.env.NODE_ENV === 'development') return; async function registerSW() { try { diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx index 97cc280..4687cb6 100644 --- a/src/components/ui/chart.tsx +++ b/src/components/ui/chart.tsx @@ -78,12 +78,9 @@ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { return null } - return ( -