fix(partner): skip unauthorized home APIs for sub-accounts
CI / verify (pull_request) Has been cancelled
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:
@@ -5,6 +5,7 @@ export function fetchPartnerLeaderboard(period: PartnerLeaderboardPeriod = 'tota
|
|||||||
return request<PartnerLeaderboardResponse>(
|
return request<PartnerLeaderboardResponse>(
|
||||||
'PARTNER_H5',
|
'PARTNER_H5',
|
||||||
`/partner/dashboard/leaderboard?period=${period}`,
|
`/partner/dashboard/leaderboard?period=${period}`,
|
||||||
|
{ silent: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,33 @@ export function hasPartnerPermission(
|
|||||||
return account.permissions?.includes(permission) ?? false;
|
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(无门店权限) */
|
/** 仓库管理员:warehouse:manage,或仅有 order:view(无门店权限) */
|
||||||
export function isWarehouseStaff(account: PartnerMe | null | undefined): boolean {
|
export function isWarehouseStaff(account: PartnerMe | null | undefined): boolean {
|
||||||
if (!account || isPrimaryAccount(account)) return false;
|
if (!account || isPrimaryAccount(account)) return false;
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import { Link, useNavigate } from 'react-router-dom';
|
|||||||
import type { PartnerLeaderboardEntry } from '@dukang/shared-types';
|
import type { PartnerLeaderboardEntry } from '@dukang/shared-types';
|
||||||
import { isLoggedIn, request } from '../lib/api';
|
import { isLoggedIn, request } from '../lib/api';
|
||||||
import { fetchPartnerLeaderboard } from '../lib/leaderboard';
|
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';
|
import { usePartnerSession } from '../contexts/PartnerSessionContext';
|
||||||
|
|
||||||
function fmtMoney(n: number) {
|
function fmtMoney(n: number) {
|
||||||
@@ -136,6 +142,9 @@ export default function HomePage() {
|
|||||||
const isPrimary = navKind === 'primary';
|
const isPrimary = navKind === 'primary';
|
||||||
const isWarehouse = navKind === 'warehouse_staff';
|
const isWarehouse = navKind === 'warehouse_staff';
|
||||||
const warehouseOk = hasWarehouseAccess(account);
|
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 [dash, setDash] = useState<Record<string, unknown> | null>(null);
|
||||||
const [stores, setStores] = useState<Array<Record<string, unknown>>>([]);
|
const [stores, setStores] = useState<Array<Record<string, unknown>>>([]);
|
||||||
@@ -148,20 +157,34 @@ export default function HomePage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoggedIn()) { navigate('/login'); return; }
|
if (!isLoggedIn()) { navigate('/login'); return; }
|
||||||
if ((isWarehouse || isPrimary) && warehouseOk) {
|
// 等 session 带上账号后再按权限发请求,避免无权限接口弹错
|
||||||
request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders')
|
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 : []))
|
.then((data) => setOrders(Array.isArray(data.list) ? data.list : []))
|
||||||
.catch(() => setOrders([]));
|
.catch(() => setOrders([]));
|
||||||
} else {
|
} else {
|
||||||
setOrders([]);
|
setOrders([]);
|
||||||
}
|
}
|
||||||
if (!isWarehouse) {
|
|
||||||
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard').then(setDash).catch(() => {});
|
if (canDashboard) {
|
||||||
request<Array<Record<string, unknown>>>('PARTNER_H5', '/partner/stores').then(setStores).catch(() => []);
|
request<Record<string, unknown>>('PARTNER_H5', '/partner/dashboard', { silent: true })
|
||||||
|
.then(setDash)
|
||||||
|
.catch(() => setDash(null));
|
||||||
} else {
|
} 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')
|
fetchPartnerLeaderboard('month')
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
setLeaderboardEntries(data.list.slice(0, LEADERBOARD_PREVIEW_LIMIT));
|
setLeaderboardEntries(data.list.slice(0, LEADERBOARD_PREVIEW_LIMIT));
|
||||||
@@ -169,7 +192,7 @@ export default function HomePage() {
|
|||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLeaderboardEntries([]);
|
setLeaderboardEntries([]);
|
||||||
});
|
});
|
||||||
}, [navigate, isWarehouse, isPrimary, warehouseOk]);
|
}, [navigate, account, canOrders, canDashboard, canStores]);
|
||||||
|
|
||||||
const storeCount = Number(dash?.storeCount || stores.length || 0);
|
const storeCount = Number(dash?.storeCount || stores.length || 0);
|
||||||
const orderStats = useMemo(() => summarizeOrders(orders), [orders]);
|
const orderStats = useMemo(() => summarizeOrders(orders), [orders]);
|
||||||
@@ -248,7 +271,7 @@ export default function HomePage() {
|
|||||||
<p className="headline-md" style={{ marginBottom: 8 }}>订单管理</p>
|
<p className="headline-md" style={{ marginBottom: 8 }}>订单管理</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(isPrimary || isWarehouse) && warehouseOk && (
|
{canOrders && (
|
||||||
<OrderSummarySection
|
<OrderSummarySection
|
||||||
title={isWarehouse ? '今日订单' : undefined}
|
title={isWarehouse ? '今日订单' : undefined}
|
||||||
todayCount={orderStats.todayCount}
|
todayCount={orderStats.todayCount}
|
||||||
@@ -270,7 +293,7 @@ export default function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isWarehouse && (
|
{canStores && (
|
||||||
<>
|
<>
|
||||||
<section className="partner-bento-card" style={{ marginBottom: 16 }}>
|
<section className="partner-bento-card" style={{ marginBottom: 16 }}>
|
||||||
<div className="partner-bento-header">
|
<div className="partner-bento-header">
|
||||||
|
|||||||
@@ -79,18 +79,17 @@ export class PartnerStoreController {
|
|||||||
|
|
||||||
@Controller('partner/dashboard')
|
@Controller('partner/dashboard')
|
||||||
@UseGuards(JwtAuthGuard, PartnerPermissionGuard)
|
@UseGuards(JwtAuthGuard, PartnerPermissionGuard)
|
||||||
@RequirePartnerPermissions('store:create', 'store:manage', 'order:view', 'warehouse:manage')
|
|
||||||
export class PartnerDashboardController {
|
export class PartnerDashboardController {
|
||||||
constructor(private readonly storeService: StoreService) {}
|
constructor(private readonly storeService: StoreService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@RequirePartnerPermissions('store:create', 'store:manage', 'order:view', 'warehouse:manage')
|
||||||
dashboard(@CurrentUser() user: AuthUser) {
|
dashboard(@CurrentUser() user: AuthUser) {
|
||||||
return this.storeService.partnerDashboard(user.actorId);
|
return this.storeService.partnerDashboard(user.actorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('leaderboard')
|
|
||||||
/** 任意合伙人子账号可看同主账号排行(激励),不校验业务权限点 */
|
/** 任意合伙人子账号可看同主账号排行(激励),不校验业务权限点 */
|
||||||
@RequirePartnerPermissions()
|
@Get('leaderboard')
|
||||||
leaderboard(
|
leaderboard(
|
||||||
@CurrentUser() user: AuthUser,
|
@CurrentUser() user: AuthUser,
|
||||||
@Query('period') period?: string,
|
@Query('period') period?: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user