From c1890560037ca5ef270a2d243f1ed5162a5a1b19 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sat, 14 Mar 2026 03:00:30 +0100 Subject: [PATCH] feat(auth): enhance error handling for token expiration and unauthorized access --- k-tv-backend/api/src/routes/channels/mod.rs | 4 +++ k-tv-frontend/app/(main)/guide/page.tsx | 13 +++++---- k-tv-frontend/app/globals.css | 18 ++++++------- k-tv-frontend/app/providers.tsx | 29 ++++++++++++++------- 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/k-tv-backend/api/src/routes/channels/mod.rs b/k-tv-backend/api/src/routes/channels/mod.rs index 29dd4da..14aee1c 100644 --- a/k-tv-backend/api/src/routes/channels/mod.rs +++ b/k-tv-backend/api/src/routes/channels/mod.rs @@ -55,6 +55,10 @@ pub(super) fn check_access( match mode { AccessMode::Public => Ok(()), AccessMode::PasswordProtected => { + // Owner always has access to their own channel without needing the password + if user.map(|u| u.id) == Some(owner_id) { + return Ok(()); + } let hash = password_hash.ok_or(ApiError::PasswordRequired)?; let supplied = supplied_password.unwrap_or("").trim(); if supplied.is_empty() { diff --git a/k-tv-frontend/app/(main)/guide/page.tsx b/k-tv-frontend/app/(main)/guide/page.tsx index 5639ecb..ba8f5b2 100644 --- a/k-tv-frontend/app/(main)/guide/page.tsx +++ b/k-tv-frontend/app/(main)/guide/page.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { useQuery } from "@tanstack/react-query"; import { Tv } from "lucide-react"; -import { api } from "@/lib/api"; +import { api, ApiRequestError } from "@/lib/api"; import { useChannels } from "@/hooks/use-channels"; import { useAuthContext } from "@/context/auth-context"; import type { ChannelResponse, ScheduledSlotResponse } from "@/lib/types"; @@ -43,16 +43,17 @@ function slotLabel(slot: ScheduledSlotResponse) { // --------------------------------------------------------------------------- function ChannelRow({ channel }: { channel: ChannelResponse }) { - const { token } = useAuthContext(); + const { token, isLoaded } = useAuthContext(); - const { data: slots, isError, isPending } = useQuery({ - queryKey: ["guide-epg", channel.id], + const { data: slots, isError, error, isPending, isFetching } = useQuery({ + queryKey: ["guide-epg", channel.id, token], queryFn: () => { const now = new Date(); const from = now.toISOString(); const until = new Date(now.getTime() + 4 * 60 * 60 * 1000).toISOString(); return api.schedule.getEpg(channel.id, token ?? "", from, until); }, + enabled: isLoaded, refetchInterval: 30_000, retry: false, }); @@ -117,8 +118,10 @@ function ChannelRow({ channel }: { channel: ChannelResponse }) { /> - ) : isPending ? ( + ) : isPending && isFetching ? (

Loading…

+ ) : isError && error instanceof ApiRequestError && error.status === 401 ? ( +

Sign in to view this channel

) : (

{isError || !slots?.length diff --git a/k-tv-frontend/app/globals.css b/k-tv-frontend/app/globals.css index 0f67ea9..4812081 100644 --- a/k-tv-frontend/app/globals.css +++ b/k-tv-frontend/app/globals.css @@ -54,7 +54,7 @@ --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); + --primary: oklch(0.702 0.176 48.5); --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); @@ -65,7 +65,7 @@ --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); + --ring: oklch(0.702 0.176 48.5); --chart-1: oklch(0.809 0.105 251.813); --chart-2: oklch(0.623 0.214 259.815); --chart-3: oklch(0.546 0.245 262.881); @@ -74,12 +74,12 @@ --radius: 0.625rem; --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary: oklch(0.702 0.176 48.5); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.97 0 0); --sidebar-accent-foreground: oklch(0.205 0 0); --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --sidebar-ring: oklch(0.702 0.176 48.5); } .dark { @@ -89,8 +89,8 @@ --card-foreground: oklch(0.985 0 0); --popover: oklch(0.205 0 0); --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); + --primary: oklch(0.702 0.176 48.5); + --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.269 0 0); @@ -100,7 +100,7 @@ --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); + --ring: oklch(0.702 0.176 48.5); --chart-1: oklch(0.809 0.105 251.813); --chart-2: oklch(0.623 0.214 259.815); --chart-3: oklch(0.546 0.245 262.881); @@ -108,12 +108,12 @@ --chart-5: oklch(0.424 0.199 265.638); --sidebar: oklch(0.205 0 0); --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary: oklch(0.702 0.176 48.5); --sidebar-primary-foreground: oklch(0.985 0 0); --sidebar-accent: oklch(0.269 0 0); --sidebar-accent-foreground: oklch(0.985 0 0); --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); + --sidebar-ring: oklch(0.702 0.176 48.5); } @layer base { diff --git a/k-tv-frontend/app/providers.tsx b/k-tv-frontend/app/providers.tsx index 245c832..384e2f5 100644 --- a/k-tv-frontend/app/providers.tsx +++ b/k-tv-frontend/app/providers.tsx @@ -14,19 +14,30 @@ import { Toaster } from "@/components/ui/sonner"; import { ApiRequestError } from "@/lib/api"; function QueryProvider({ children }: { children: React.ReactNode }) { - const { setToken } = useAuthContext(); + const { token, setToken } = useAuthContext(); const router = useRouter(); const [queryClient] = useState(() => { - const on401 = (error: unknown) => { - if (error instanceof ApiRequestError && error.status === 401) { - setToken(null); - router.push("/login"); - } - }; return new QueryClient({ - queryCache: new QueryCache({ onError: on401 }), - mutationCache: new MutationCache({ onError: on401 }), + queryCache: new QueryCache({ + onError: (error) => { + // Only redirect on 401 if the user had a token (expired session). + // Guests hitting 401 on restricted content should not be redirected. + if (error instanceof ApiRequestError && error.status === 401 && token) { + setToken(null); + router.push("/login"); + } + }, + }), + mutationCache: new MutationCache({ + onError: (error) => { + // Mutations always require auth — redirect on 401 regardless. + if (error instanceof ApiRequestError && error.status === 401) { + setToken(null); + router.push("/login"); + } + }, + }), defaultOptions: { queries: { staleTime: 60 * 1000 } }, }); });