From a64e8f17a195c9da74cfb6c7e66abf89d1790889 Mon Sep 17 00:00:00 2001 From: robin Date: Thu, 18 Dec 2025 21:02:44 +0100 Subject: [PATCH] feat: session deletion, improved tasting deletion visibility, and PWA login loop fix --- public/sw.js | 28 ++++++++++++++++++--- src/app/bottles/[id]/page.tsx | 3 ++- src/app/page.tsx | 23 +++++++++++++---- src/app/sessions/[id]/page.tsx | 28 +++++++++++++++++++++ src/components/SessionList.tsx | 39 ++++++++++++++++++++++++++++- src/components/TastingList.tsx | 45 +++++++++++++++++++--------------- src/middleware.ts | 5 ++-- src/services/delete-session.ts | 35 ++++++++++++++++++++++++++ 8 files changed, 174 insertions(+), 32 deletions(-) create mode 100644 src/services/delete-session.ts diff --git a/public/sw.js b/public/sw.js index 71feb1e..52d3c5f 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,6 +1,5 @@ -const CACHE_NAME = 'whisky-vault-v1'; +const CACHE_NAME = 'whisky-vault-v2'; // Increment version to force update const ASSETS_TO_CACHE = [ - '/', '/manifest.json', '/icon-192.png', '/icon-512.png', @@ -27,13 +26,36 @@ self.addEventListener('activate', (event) => { ); }) ); + self.clients.claim(); }); self.addEventListener('fetch', (event) => { - // Network first, fallback to cache + const url = new URL(event.request.url); + + // CRITICAL: Always bypass cache for auth, api, and supabase requests + if ( + url.pathname.includes('/auth/') || + url.pathname.includes('/api/') || + url.hostname.includes('supabase.co') + ) { + return; // Let it fall through to the network + } + + // Network first for all other requests, especially navigation event.respondWith( fetch(event.request) .then((response) => { + // Optionally cache successful GET requests for assets + if ( + event.request.method === 'GET' && + response.status === 200 && + (url.pathname.startsWith('/_next/static/') || ASSETS_TO_CACHE.includes(url.pathname)) + ) { + const responseClone = response.clone(); + caches.open(CACHE_NAME).then((cache) => { + cache.put(event.request, responseClone); + }); + } return response; }) .catch(() => { diff --git a/src/app/bottles/[id]/page.tsx b/src/app/bottles/[id]/page.tsx index 69bc618..49e6d40 100644 --- a/src/app/bottles/[id]/page.tsx +++ b/src/app/bottles/[id]/page.tsx @@ -28,6 +28,7 @@ export default async function BottlePage({ } } const supabase = createServerComponentClient({ cookies }); + const { data: { user } } = await supabase.auth.getUser(); const { data: bottle } = await supabase .from('bottles') @@ -187,7 +188,7 @@ export default async function BottlePage({ {/* List */}
- +
diff --git a/src/app/page.tsx b/src/app/page.tsx index e379467..77c2b6d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -25,16 +25,22 @@ export default function Home() { const checkUser = async () => { try { const { data: { session }, error } = await supabase.auth.getSession(); - console.log('Initial session check:', session ? 'User logged in' : 'No session'); + console.log('[Auth] Initial session check:', { + hasSession: !!session, + userId: session?.user?.id, + email: session?.user?.email, + error: error?.message + }); + if (error) { - console.error('Session retrieval error:', error); + console.error('[Auth] Session retrieval error:', error); } setUser(session?.user ?? null); if (session?.user) { fetchCollection(); } } catch (err) { - console.error('Fatal error checking user:', err); + console.error('[Auth] Fatal error checking user:', err); setUser(null); } finally { setIsLoading(false); @@ -45,10 +51,17 @@ export default function Home() { // Listen for auth changes const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => { - console.log('Auth state change:', event, session?.user ? 'User logged in' : 'No user'); + console.log('[Auth] State change event:', event, { + hasSession: !!session, + userId: session?.user?.id, + email: session?.user?.email + }); + setUser(session?.user ?? null); if (session?.user) { - fetchCollection(); + if (event === 'SIGNED_IN' || event === 'INITIAL_SESSION' || event === 'TOKEN_REFRESHED') { + fetchCollection(); + } } else { setBottles([]); } diff --git a/src/app/sessions/[id]/page.tsx b/src/app/sessions/[id]/page.tsx index beba47d..610aa16 100644 --- a/src/app/sessions/[id]/page.tsx +++ b/src/app/sessions/[id]/page.tsx @@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react'; import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; import { ChevronLeft, Users, Calendar, GlassWater, Plus, Trash2, Loader2, Sparkles, ChevronRight, Play, Square } from 'lucide-react'; import Link from 'next/link'; +import { deleteSession } from '@/services/delete-session'; import { useSession } from '@/context/SessionContext'; import { useParams, useRouter } from 'next/navigation'; import { useI18n } from '@/i18n/I18nContext'; @@ -48,6 +49,7 @@ export default function SessionDetailPage() { const [isLoading, setIsLoading] = useState(true); const { activeSession, setActiveSession } = useSession(); const [isAddingParticipant, setIsAddingParticipant] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); useEffect(() => { fetchSessionData(); @@ -131,6 +133,23 @@ export default function SessionDetailPage() { } }; + const handleDeleteSession = async () => { + if (!confirm('Möchtest du diese Session wirklich löschen? Alle Verknüpfungen gehen verloren.')) return; + + setIsDeleting(true); + const result = await deleteSession(id as string); + + if (result.success) { + if (activeSession?.id === id) { + setActiveSession(null); + } + router.push('/'); + } else { + alert(result.error); + setIsDeleting(false); + } + }; + if (isLoading) { return (
@@ -206,6 +225,15 @@ export default function SessionDetailPage() { Session Stoppen )} + +
diff --git a/src/components/SessionList.tsx b/src/components/SessionList.tsx index 62d77eb..222577f 100644 --- a/src/components/SessionList.tsx +++ b/src/components/SessionList.tsx @@ -2,8 +2,9 @@ import React, { useState, useEffect } from 'react'; import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; -import { Calendar, Plus, GlassWater, Loader2, ChevronRight, Users, Check } from 'lucide-react'; +import { Calendar, Plus, GlassWater, Loader2, ChevronRight, Users, Check, Trash2 } from 'lucide-react'; import Link from 'next/link'; +import { deleteSession } from '@/services/delete-session'; import { useI18n } from '@/i18n/I18nContext'; import { useSession } from '@/context/SessionContext'; @@ -21,6 +22,7 @@ export default function SessionList() { const [sessions, setSessions] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isCreating, setIsCreating] = useState(false); + const [isDeleting, setIsDeleting] = useState(null); const [newName, setNewName] = useState(''); const { activeSession, setActiveSession } = useSession(); @@ -92,6 +94,26 @@ export default function SessionList() { setIsCreating(false); }; + const handleDeleteSession = async (e: React.MouseEvent, sessionId: string) => { + e.preventDefault(); + e.stopPropagation(); + + if (!confirm('Möchtest du diese Session wirklich löschen? Alle Verknüpfungen gehen verloren.')) return; + + setIsDeleting(sessionId); + const result = await deleteSession(sessionId); + + if (result.success) { + setSessions(prev => prev.filter(s => s.id !== sessionId)); + if (activeSession?.id === sessionId) { + setActiveSession(null); + } + } else { + alert(result.error); + } + setIsDeleting(null); + }; + return (

@@ -172,6 +194,21 @@ export default function SessionList() {

)} + )) diff --git a/src/components/TastingList.tsx b/src/components/TastingList.tsx index 4d4ae48..faaeba3 100644 --- a/src/components/TastingList.tsx +++ b/src/components/TastingList.tsx @@ -24,13 +24,15 @@ interface Tasting { id: string; name: string; }; + user_id: string; } interface TastingListProps { initialTastings: Tasting[]; + currentUserId?: string; } -export default function TastingList({ initialTastings }: TastingListProps) { +export default function TastingList({ initialTastings, currentUserId }: TastingListProps) { const [sortBy, setSortBy] = useState<'date-desc' | 'date-asc' | 'rating-desc' | 'rating-asc'>('date-desc'); const [isDeleting, setIsDeleting] = useState(null); @@ -100,8 +102,8 @@ export default function TastingList({ initialTastings }: TastingListProps) { key={note.id} className="bg-white dark:bg-zinc-900 p-6 rounded-3xl border border-zinc-200 dark:border-zinc-800 shadow-sm space-y-4 hover:border-amber-500/30 transition-all hover:shadow-md group" > -
-
+
+
{note.rating}/100 @@ -113,38 +115,41 @@ export default function TastingList({ initialTastings }: TastingListProps) { {note.is_sample ? 'Sample' : 'Bottle'}
+ {new Date(note.created_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
{note.tasting_sessions && ( {note.tasting_sessions.name} )}
-
+
{new Date(note.created_at).toLocaleDateString('de-DE')}
- + {(!currentUserId || note.user_id === currentUserId) && ( + + )}
diff --git a/src/middleware.ts b/src/middleware.ts index d2d84c5..e9e31f0 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -16,10 +16,11 @@ export async function middleware(req: NextRequest) { const { data: { session } } = await supabase.auth.getSession(); if (!isStatic) { - console.log(`[Middleware] Path: ${url.pathname}, Session: ${session ? 'Active' : 'Missing'}, User: ${session?.user?.id || 'N/A'}`); + const status = session ? `User:${session.user.id.slice(0, 8)}` : 'No Session'; + console.log(`[MW] ${req.method} ${url.pathname} | ${status}`); } } catch (e) { - console.error('Middleware session refresh failed:', e); + console.error('[MW] Auth Error:', e); } } diff --git a/src/services/delete-session.ts b/src/services/delete-session.ts new file mode 100644 index 0000000..175638a --- /dev/null +++ b/src/services/delete-session.ts @@ -0,0 +1,35 @@ +'use server'; + +import { createServerActionClient } from '@supabase/auth-helpers-nextjs'; +import { cookies } from 'next/headers'; +import { revalidatePath } from 'next/cache'; + +export async function deleteSession(sessionId: string) { + const supabase = createServerActionClient({ cookies }); + + try { + const { data: { session } } = await supabase.auth.getSession(); + if (!session) { + throw new Error('Nicht autorisiert.'); + } + + const { error: deleteError } = await supabase + .from('tasting_sessions') + .delete() + .eq('id', sessionId) + .eq('user_id', session.user.id); + + if (deleteError) throw deleteError; + + revalidatePath('/'); + revalidatePath(`/sessions/${sessionId}`); + + return { success: true }; + } catch (error) { + console.error('Delete Session Error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Fehler beim Löschen der Session.', + }; + } +}