feat(auth): enhance error handling for token expiration and unauthorized access
This commit is contained in:
@@ -55,6 +55,10 @@ pub(super) fn check_access(
|
|||||||
match mode {
|
match mode {
|
||||||
AccessMode::Public => Ok(()),
|
AccessMode::Public => Ok(()),
|
||||||
AccessMode::PasswordProtected => {
|
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 hash = password_hash.ok_or(ApiError::PasswordRequired)?;
|
||||||
let supplied = supplied_password.unwrap_or("").trim();
|
let supplied = supplied_password.unwrap_or("").trim();
|
||||||
if supplied.is_empty() {
|
if supplied.is_empty() {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Tv } from "lucide-react";
|
import { Tv } from "lucide-react";
|
||||||
import { api } from "@/lib/api";
|
import { api, ApiRequestError } from "@/lib/api";
|
||||||
import { useChannels } from "@/hooks/use-channels";
|
import { useChannels } from "@/hooks/use-channels";
|
||||||
import { useAuthContext } from "@/context/auth-context";
|
import { useAuthContext } from "@/context/auth-context";
|
||||||
import type { ChannelResponse, ScheduledSlotResponse } from "@/lib/types";
|
import type { ChannelResponse, ScheduledSlotResponse } from "@/lib/types";
|
||||||
@@ -43,16 +43,17 @@ function slotLabel(slot: ScheduledSlotResponse) {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function ChannelRow({ channel }: { channel: ChannelResponse }) {
|
function ChannelRow({ channel }: { channel: ChannelResponse }) {
|
||||||
const { token } = useAuthContext();
|
const { token, isLoaded } = useAuthContext();
|
||||||
|
|
||||||
const { data: slots, isError, isPending } = useQuery({
|
const { data: slots, isError, error, isPending, isFetching } = useQuery({
|
||||||
queryKey: ["guide-epg", channel.id],
|
queryKey: ["guide-epg", channel.id, token],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const from = now.toISOString();
|
const from = now.toISOString();
|
||||||
const until = new Date(now.getTime() + 4 * 60 * 60 * 1000).toISOString();
|
const until = new Date(now.getTime() + 4 * 60 * 60 * 1000).toISOString();
|
||||||
return api.schedule.getEpg(channel.id, token ?? "", from, until);
|
return api.schedule.getEpg(channel.id, token ?? "", from, until);
|
||||||
},
|
},
|
||||||
|
enabled: isLoaded,
|
||||||
refetchInterval: 30_000,
|
refetchInterval: 30_000,
|
||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
@@ -117,8 +118,10 @@ function ChannelRow({ channel }: { channel: ChannelResponse }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : isPending ? (
|
) : isPending && isFetching ? (
|
||||||
<p className="text-xs italic text-zinc-600">Loading…</p>
|
<p className="text-xs italic text-zinc-600">Loading…</p>
|
||||||
|
) : isError && error instanceof ApiRequestError && error.status === 401 ? (
|
||||||
|
<p className="text-xs italic text-zinc-600">Sign in to view this channel</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs italic text-zinc-600">
|
<p className="text-xs italic text-zinc-600">
|
||||||
{isError || !slots?.length
|
{isError || !slots?.length
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.145 0 0);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.145 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);
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
--secondary: oklch(0.97 0 0);
|
--secondary: oklch(0.97 0 0);
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: oklch(0.922 0 0);
|
--border: oklch(0.922 0 0);
|
||||||
--input: 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-1: oklch(0.809 0.105 251.813);
|
||||||
--chart-2: oklch(0.623 0.214 259.815);
|
--chart-2: oklch(0.623 0.214 259.815);
|
||||||
--chart-3: oklch(0.546 0.245 262.881);
|
--chart-3: oklch(0.546 0.245 262.881);
|
||||||
@@ -74,12 +74,12 @@
|
|||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--sidebar: oklch(0.985 0 0);
|
--sidebar: oklch(0.985 0 0);
|
||||||
--sidebar-foreground: oklch(0.145 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-primary-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
--sidebar-border: oklch(0.922 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 {
|
.dark {
|
||||||
@@ -89,8 +89,8 @@
|
|||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.985 0 0);
|
||||||
--popover: oklch(0.205 0 0);
|
--popover: oklch(0.205 0 0);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
--primary: oklch(0.922 0 0);
|
--primary: oklch(0.702 0.176 48.5);
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
--secondary: oklch(0.269 0 0);
|
--secondary: oklch(0.269 0 0);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
--muted: oklch(0.269 0 0);
|
--muted: oklch(0.269 0 0);
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 10%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--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-1: oklch(0.809 0.105 251.813);
|
||||||
--chart-2: oklch(0.623 0.214 259.815);
|
--chart-2: oklch(0.623 0.214 259.815);
|
||||||
--chart-3: oklch(0.546 0.245 262.881);
|
--chart-3: oklch(0.546 0.245 262.881);
|
||||||
@@ -108,12 +108,12 @@
|
|||||||
--chart-5: oklch(0.424 0.199 265.638);
|
--chart-5: oklch(0.424 0.199 265.638);
|
||||||
--sidebar: oklch(0.205 0 0);
|
--sidebar: oklch(0.205 0 0);
|
||||||
--sidebar-foreground: oklch(0.985 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-primary-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--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 {
|
@layer base {
|
||||||
|
|||||||
@@ -14,19 +14,30 @@ import { Toaster } from "@/components/ui/sonner";
|
|||||||
import { ApiRequestError } from "@/lib/api";
|
import { ApiRequestError } from "@/lib/api";
|
||||||
|
|
||||||
function QueryProvider({ children }: { children: React.ReactNode }) {
|
function QueryProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { setToken } = useAuthContext();
|
const { token, setToken } = useAuthContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [queryClient] = useState(() => {
|
const [queryClient] = useState(() => {
|
||||||
const on401 = (error: unknown) => {
|
|
||||||
if (error instanceof ApiRequestError && error.status === 401) {
|
|
||||||
setToken(null);
|
|
||||||
router.push("/login");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return new QueryClient({
|
return new QueryClient({
|
||||||
queryCache: new QueryCache({ onError: on401 }),
|
queryCache: new QueryCache({
|
||||||
mutationCache: new MutationCache({ onError: on401 }),
|
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 } },
|
defaultOptions: { queries: { staleTime: 60 * 1000 } },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user