diff --git a/public/robots.txt b/public/robots.txt index d99bbc0e..a3e6a331 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,3 +1,5 @@ User-agent: * -Allow: /* +Disallow: /api/ +Disallow: /login +Disallow: /settings Sitemap: https://bittorrented.com/sitemap.xml diff --git a/src/app/live-tv/page.tsx b/src/app/live-tv/page.tsx index 17c66554..2f8f067d 100644 --- a/src/app/live-tv/page.tsx +++ b/src/app/live-tv/page.tsx @@ -1,10 +1,10 @@ /** * Live TV Page (Server Component) * - * Server-side auth check - redirects to login if not authenticated. + * Shows login prompt if not authenticated, otherwise shows Live TV content. */ -import { redirect } from 'next/navigation'; +import Link from 'next/link'; import { getCurrentUser } from '@/lib/auth'; import { LiveTvContent } from './live-tv-content'; @@ -19,7 +19,20 @@ export default async function LiveTvPage(): Promise { const user = await getCurrentUser(); if (!user) { - redirect('/login?redirect=/live-tv&reason=live-tv'); + return ( +
+

Login Required

+

+ You need to be logged in to access Live TV. Sign in to stream live channels from your IPTV playlists. +

+ + Sign In + +
+ ); } return ; diff --git a/src/components/home/quick-actions.tsx b/src/components/home/quick-actions.tsx index f598b490..b53656a8 100644 --- a/src/components/home/quick-actions.tsx +++ b/src/components/home/quick-actions.tsx @@ -8,6 +8,8 @@ */ import { useState, useCallback } from 'react'; +import { useRouter } from 'next/navigation'; +import { useAuth } from '@/hooks/use-auth'; import Link from 'next/link'; import { MagnetIcon, SearchIcon, MusicIcon, VideoIcon } from '@/components/ui/icons'; import { AddMagnetModal } from '@/components/torrents/add-magnet-modal'; @@ -63,10 +65,16 @@ function QuickActionCard({ export function QuickActions(): React.ReactElement { const [isModalOpen, setIsModalOpen] = useState(false); + const { isLoggedIn } = useAuth(); + const router = useRouter(); const handleOpenModal = useCallback((): void => { + if (!isLoggedIn) { + router.push('/login'); + return; + } setIsModalOpen(true); - }, []); + }, [isLoggedIn, router]); const handleCloseModal = useCallback((): void => { setIsModalOpen(false); @@ -78,8 +86,8 @@ export function QuickActions(): React.ReactElement { + © {new Date().getFullYear()} Media Streamer + · { expect(link).toHaveAttribute('href', '/login'); }); + it('should show lock icon on auth-required items when not logged in', () => { + render(); + + // Auth-required items should have a lock icon with "Login required" title + const lockIcons = screen.getAllByTitle('Login required'); + expect(lockIcons.length).toBeGreaterThan(0); + }); + + it('should not show lock icon on auth-required items when logged in and premium', () => { + render(); + + const lockIcons = screen.queryAllByTitle('Login required'); + expect(lockIcons.length).toBe(0); + }); + it('should show My Library link with correct href when user IS logged in', () => { render(); diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index 2366ad33..91bdd504 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -224,15 +224,15 @@ function NavSection({ items, pathname, onItemClick, isLoggedIn, isPremium }: Nav {items.map((item) => { const isActive = pathname === item.href; const Icon = item.icon; - // Redirect to login if auth-required and not logged in, or paid feature and not premium + // Show lock icon for auth-required items when not logged in const needsLogin = (item.requiresAuth && !isLoggedIn) || (item.requiresPaid && !isPremium); - const href = needsLogin ? '/login' : item.href; return (
  • {item.label} + {needsLogin ? 🔒 : null} {item.badge ? {item.badge} : null} diff --git a/src/hooks/use-supported-coins.test.ts b/src/hooks/use-supported-coins.test.ts index f1013c54..40041f44 100644 --- a/src/hooks/use-supported-coins.test.ts +++ b/src/hooks/use-supported-coins.test.ts @@ -55,7 +55,7 @@ describe('useSupportedCoins', () => { expect(result.current.coins).toEqual(mockCoins); expect(result.current.error).toBeNull(); - expect(mockFetch).toHaveBeenCalledWith('/api/supported-coins?active_only=true'); + expect(mockFetch).toHaveBeenCalledWith('/api/supported-coins?active_only=true', expect.objectContaining({ signal: expect.any(AbortSignal) })); }); it('should filter active coins by default', async () => { @@ -72,7 +72,7 @@ describe('useSupportedCoins', () => { renderHook(() => useSupportedCoins()); await waitFor(() => { - expect(mockFetch).toHaveBeenCalledWith('/api/supported-coins?active_only=true'); + expect(mockFetch).toHaveBeenCalledWith('/api/supported-coins?active_only=true', expect.objectContaining({ signal: expect.any(AbortSignal) })); }); }); @@ -90,7 +90,7 @@ describe('useSupportedCoins', () => { renderHook(() => useSupportedCoins({ activeOnly: false })); await waitFor(() => { - expect(mockFetch).toHaveBeenCalledWith('/api/supported-coins?active_only=false'); + expect(mockFetch).toHaveBeenCalledWith('/api/supported-coins?active_only=false', expect.objectContaining({ signal: expect.any(AbortSignal) })); }); }); diff --git a/src/hooks/use-supported-coins.ts b/src/hooks/use-supported-coins.ts index 196d214c..d6d35b7a 100644 --- a/src/hooks/use-supported-coins.ts +++ b/src/hooks/use-supported-coins.ts @@ -63,9 +63,14 @@ export function useSupportedCoins( setError(null); try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); + const response = await fetch( - `/api/supported-coins?active_only=${activeOnly}` + `/api/supported-coins?active_only=${activeOnly}`, + { signal: controller.signal } ); + clearTimeout(timeoutId); const data = (await response.json()) as | SupportedCoinsResponse @@ -79,7 +84,10 @@ export function useSupportedCoins( const successData = data as SupportedCoinsResponse; setCoins(successData.coins); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to fetch supported coins'); + const message = err instanceof Error && err.name === 'AbortError' + ? 'Payment methods unavailable. Please try again later.' + : err instanceof Error ? err.message : 'Failed to fetch supported coins'; + setError(message); setCoins([]); } finally { setIsLoading(false);