fix(partner): skip unauthorized home APIs for sub-accounts
CI / verify (pull_request) Has been cancelled

Gate dashboard/stores/orders by permission and move dashboard auth to method level so leaderboard stays open without toasting 403s.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-17 10:13:43 +08:00
parent 04253b9120
commit 55e91158b3
4 changed files with 64 additions and 14 deletions
+1
View File
@@ -5,6 +5,7 @@ export function fetchPartnerLeaderboard(period: PartnerLeaderboardPeriod = 'tota
return request<PartnerLeaderboardResponse>(
'PARTNER_H5',
`/partner/dashboard/leaderboard?period=${period}`,
{ silent: true },
);
}
+27
View File
@@ -19,6 +19,33 @@ export function hasPartnerPermission(
return account.permissions?.includes(permission) ?? false;
}
export function hasAnyPartnerPermission(
account: PartnerMe | null | undefined,
permissions: PartnerPermissionKey[],
): boolean {
return permissions.some((p) => hasPartnerPermission(account, p));
}
/** 与后端 GET /partner/dashboard 权限点一致 */
export function canAccessPartnerDashboard(account: PartnerMe | null | undefined): boolean {
return hasAnyPartnerPermission(account, [
'store:create',
'store:manage',
'order:view',
'warehouse:manage',
]);
}
/** 门店列表/拓店统计所需权限 */
export function canAccessPartnerStores(account: PartnerMe | null | undefined): boolean {
return hasAnyPartnerPermission(account, ['store:create', 'store:manage']);
}
/** 与后端 GET /partner/orders 权限点一致 */
export function canAccessPartnerOrders(account: PartnerMe | null | undefined): boolean {
return hasAnyPartnerPermission(account, ['order:view', 'warehouse:manage']);
}
/** 仓库管理员:warehouse:manage,或仅有 order:view(无门店权限) */
export function isWarehouseStaff(account: PartnerMe | null | undefined): boolean {
if (!account || isPrimaryAccount(account)) return false;
+34 -11
View File
@@ -3,7 +3,13 @@ import { Link, useNavigate } from 'react-router-dom';
import type { PartnerLeaderboardEntry } from '@dukang/shared-types';
import { isLoggedIn, request } from '../lib/api';
import { fetchPartnerLeaderboard } from '../lib/leaderboard';
import { getPartnerNavKind, hasWarehouseAccess } from '../lib/partnerAccess';
import {
canAccessPartnerDashboard,
canAccessPartnerOrders,
canAccessPartnerStores,
getPartnerNavKind,
hasWarehouseAccess,
} from '../lib/partnerAccess';
import { usePartnerSession } from '../contexts/PartnerSessionContext';
function fmtMoney(n: number) {
@@ -136,6 +142,9 @@ export default function HomePage() {
const isPrimary = navKind === 'primary';
const isWarehouse = navKind === 'warehouse_staff';
const warehouseOk = hasWarehouseAccess(account);
const canOrders = canAccessPartnerOrders(account) && warehouseOk && (isPrimary || isWarehouse);
const canDashboard = canAccessPartnerDashboard(account) && !isWarehouse;
const canStores = canAccessPartnerStores(account) && !isWarehouse;
const [dash, setDash] = useState<Record<string, unknown> | null>(null);
const [stores, setStores] = useState<Array<Record<string, unknown>>>([]);
@@ -148,20 +157,34 @@ export default function HomePage() {
useEffect(() => {
if (!isLoggedIn()) { navigate('/login'); return; }
if ((isWarehouse || isPrimary) && warehouseOk) {
request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders')
// 等 session 带上账号后再按权限发请求,避免无权限接口弹错
if (!account) return;
if (canOrders) {
request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders', { silent: true })
.then((data) => setOrders(Array.isArray(data.list) ? data.list : []))
.catch(() => setOrders([]));
} else {
setOrders([]);
}
if (!isWarehouse) {
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard').then(setDash).catch(() => {});
request<Array<Record<string, unknown>>>('PARTNER_H5', '/partner/stores').then(setStores).catch(() => []);
if (canDashboard) {
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard', { silent: true })
.then(setDash)
.catch(() => setDash(null));
} else {
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard').then(setDash).catch(() => {});
setDash(null);
}
// 主账号与全部子账号均可查看同团队贡献榜
if (canStores) {
request<Array<Record<string, unknown>>>('PARTNER_H5', '/partner/stores', { silent: true })
.then((data) => setStores(Array.isArray(data) ? data : []))
.catch(() => setStores([]));
} else {
setStores([]);
}
// 主账号与全部子账号均可查看同团队贡献榜(后端不校验业务权限点)
fetchPartnerLeaderboard('month')
.then((data) => {
setLeaderboardEntries(data.list.slice(0, LEADERBOARD_PREVIEW_LIMIT));
@@ -169,7 +192,7 @@ export default function HomePage() {
.catch(() => {
setLeaderboardEntries([]);
});
}, [navigate, isWarehouse, isPrimary, warehouseOk]);
}, [navigate, account, canOrders, canDashboard, canStores]);
const storeCount = Number(dash?.storeCount || stores.length || 0);
const orderStats = useMemo(() => summarizeOrders(orders), [orders]);
@@ -248,7 +271,7 @@ export default function HomePage() {
<p className="headline-md" style={{ marginBottom: 8 }}></p>
)}
{(isPrimary || isWarehouse) && warehouseOk && (
{canOrders && (
<OrderSummarySection
title={isWarehouse ? '今日订单' : undefined}
todayCount={orderStats.todayCount}
@@ -270,7 +293,7 @@ export default function HomePage() {
</section>
)}
{!isWarehouse && (
{canStores && (
<>
<section className="partner-bento-card" style={{ marginBottom: 16 }}>
<div className="partner-bento-header">