fix(seller-team): guard owner-only routes + robust useMyStores
Review fixes: - useMyStores throws on a failed response instead of resolving to [] — a transient 401/blip no longer looks like "no stores" and bounces an owner off their own dashboard (query stays in error/retry, keeps prior data). - Add useRequireOwner() guard and apply it to the owner-only routes (financial-dashboard + its cash-funds/reports/transactions sub-routes and shipping-method) so staff who hit those URLs directly are redirected to the dashboard. (Financial data was already backend-protected; this is the matching client-side gate.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a642fc0fae
commit
bb44ebf0e5
@ -38,3 +38,21 @@ export function useStoreOwnership(storeId: string | undefined, mode?: string) {
|
|||||||
stores: stores ?? [],
|
stores: stores ?? [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guard for OWNER-ONLY store pages (financial, shipping, edit, team). Redirects
|
||||||
|
* non-members (via useStoreOwnership) and staff members back to the dashboard.
|
||||||
|
* Returns { isLoading, isOwner } so callers can render a spinner meanwhile.
|
||||||
|
*/
|
||||||
|
export function useRequireOwner(storeId: string | undefined) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { isLoading, role } = useStoreOwnership(storeId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading && role && role !== "owner") {
|
||||||
|
navigate(`/store/${storeId}`);
|
||||||
|
}
|
||||||
|
}, [isLoading, role, storeId, navigate]);
|
||||||
|
|
||||||
|
return { isLoading, isOwner: role === "owner" };
|
||||||
|
}
|
||||||
|
|||||||
@ -45,10 +45,14 @@ export function useMyStores() {
|
|||||||
const res = await fetch(`${getApiBaseUrl()}/api/seller/my-stores/`, {
|
const res = await fetch(`${getApiBaseUrl()}/api/seller/my-stores/`, {
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
if (!res.ok) return [];
|
// Throw on failure so a transient error stays an ERROR state (data kept,
|
||||||
|
// retried) instead of resolving to [] — otherwise a blip would look like
|
||||||
|
// "no stores" and bounce an owner off their own dashboard.
|
||||||
|
if (!res.ok) throw new Error(`my-stores failed: ${res.status}`);
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
enabled: !!token,
|
enabled: !!token,
|
||||||
|
retry: 1,
|
||||||
staleTime: 60 * 1000,
|
staleTime: 60 * 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
HandCoins,
|
HandCoins,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import HeaderWithSupport from "../components/HeaderWithSupport";
|
import HeaderWithSupport from "../components/HeaderWithSupport";
|
||||||
|
import { useRequireOwner } from "../hooks/useStoreOwnership";
|
||||||
|
|
||||||
export const meta: MetaFunction = () => {
|
export const meta: MetaFunction = () => {
|
||||||
return [
|
return [
|
||||||
@ -41,6 +42,7 @@ export const meta: MetaFunction = () => {
|
|||||||
|
|
||||||
export default function FinancialDashboard() {
|
export default function FinancialDashboard() {
|
||||||
const { storeId } = useParams();
|
const { storeId } = useParams();
|
||||||
|
useRequireOwner(storeId); // financial is owner-only
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
safeGoBack(navigate);
|
safeGoBack(navigate);
|
||||||
|
|||||||
@ -16,8 +16,9 @@ import {
|
|||||||
CarouselItem,
|
CarouselItem,
|
||||||
} from "../components/ui/carousel";
|
} from "../components/ui/carousel";
|
||||||
import { useCallback, useState, useRef, useEffect } from "react";
|
import { useCallback, useState, useRef, useEffect } from "react";
|
||||||
import { useNavigate } from "@remix-run/react";
|
import { useNavigate, useParams } from "@remix-run/react";
|
||||||
import { useCarousel } from "../hooks/useCarousel";
|
import { useCarousel } from "../hooks/useCarousel";
|
||||||
|
import { useRequireOwner } from "../hooks/useStoreOwnership";
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
DrawerTitle,
|
DrawerTitle,
|
||||||
@ -50,6 +51,8 @@ import { BankLogo } from "~/components/BankLogo";
|
|||||||
|
|
||||||
export default function CashingFunds() {
|
export default function CashingFunds() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { storeId } = useParams();
|
||||||
|
useRequireOwner(storeId); // financial is owner-only
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
safeGoBack(navigate);
|
safeGoBack(navigate);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,14 +2,17 @@ import { Sun, Waves, Loader2 } from "lucide-react";
|
|||||||
import HeaderWithSupport from "../components/HeaderWithSupport";
|
import HeaderWithSupport from "../components/HeaderWithSupport";
|
||||||
import PersianDatePicker from "../components/PersianDatePicker";
|
import PersianDatePicker from "../components/PersianDatePicker";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSearchParams, useNavigate } from "react-router-dom";
|
import { useSearchParams, useNavigate, useParams } from "react-router-dom";
|
||||||
import { formatPrice, safeGoBack } from "../utils/helpers";
|
import { formatPrice, safeGoBack } from "../utils/helpers";
|
||||||
import { useServiceGetReport } from "../requestHandler/use-wallet-hooks";
|
import { useServiceGetReport } from "../requestHandler/use-wallet-hooks";
|
||||||
|
import { useRequireOwner } from "../hooks/useStoreOwnership";
|
||||||
import jMoment from "moment-jalaali";
|
import jMoment from "moment-jalaali";
|
||||||
import { MetaFunction } from "@remix-run/node";
|
import { MetaFunction } from "@remix-run/node";
|
||||||
|
|
||||||
export default function Reports() {
|
export default function Reports() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { storeId } = useParams();
|
||||||
|
useRequireOwner(storeId); // financial is owner-only
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
safeGoBack(navigate);
|
safeGoBack(navigate);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -12,8 +12,9 @@ import {
|
|||||||
} from "../components/ui/drawer";
|
} from "../components/ui/drawer";
|
||||||
import { RadioGroup, RadioGroupItem } from "../components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "../components/ui/radio-group";
|
||||||
import PersianDatePicker from "../components/PersianDatePicker";
|
import PersianDatePicker from "../components/PersianDatePicker";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams, useParams } from "react-router-dom";
|
||||||
import { useServiceGetPayoutRequests } from "../requestHandler/use-wallet-hooks";
|
import { useServiceGetPayoutRequests } from "../requestHandler/use-wallet-hooks";
|
||||||
|
import { useRequireOwner } from "../hooks/useStoreOwnership";
|
||||||
import jMoment from "moment-jalaali";
|
import jMoment from "moment-jalaali";
|
||||||
import filterIcon from "~/assets/icons/filter.png";
|
import filterIcon from "~/assets/icons/filter.png";
|
||||||
import filterIconDark from "~/assets/icons/filter-dark.png";
|
import filterIconDark from "~/assets/icons/filter-dark.png";
|
||||||
@ -51,6 +52,8 @@ export const meta: MetaFunction = () => {
|
|||||||
|
|
||||||
export default function Transactions() {
|
export default function Transactions() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { storeId } = useParams();
|
||||||
|
useRequireOwner(storeId); // financial is owner-only
|
||||||
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false);
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { theme } = useRootData();
|
const { theme } = useRootData();
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import {
|
|||||||
import { SellerCourierCreate, SellerCourierUpdate } from "../../src/api/types";
|
import { SellerCourierCreate, SellerCourierUpdate } from "../../src/api/types";
|
||||||
import { useToast } from "../hooks/use-toast";
|
import { useToast } from "../hooks/use-toast";
|
||||||
import { useParams, useNavigate } from "@remix-run/react";
|
import { useParams, useNavigate } from "@remix-run/react";
|
||||||
import { useStoreOwnership } from "../hooks/useStoreOwnership";
|
import { useRequireOwner } from "../hooks/useStoreOwnership";
|
||||||
import { MetaFunction } from "@remix-run/node";
|
import { MetaFunction } from "@remix-run/node";
|
||||||
import { getPersianTextOfPrice, safeGoBack } from "~/utils/helpers";
|
import { getPersianTextOfPrice, safeGoBack } from "~/utils/helpers";
|
||||||
|
|
||||||
@ -37,8 +37,8 @@ export default function ShippingMethod() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [savingCourierId, setSavingCourierId] = useState<string | null>(null);
|
const [savingCourierId, setSavingCourierId] = useState<string | null>(null);
|
||||||
|
|
||||||
// Check store ownership - this will redirect if user doesn't own the store
|
// Shipping/courier config is owner-only — redirect staff back to the dashboard.
|
||||||
const { isLoading: isOwnershipLoading } = useStoreOwnership(storeId);
|
const { isLoading: isOwnershipLoading } = useRequireOwner(storeId);
|
||||||
|
|
||||||
// Fetch all available couriers
|
// Fetch all available couriers
|
||||||
const {
|
const {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user