baseline: Clover 独立仓库首次基线提交
将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。 当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包), M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
76
frontend/src/components/AuthGuard.tsx
Normal file
76
frontend/src/components/AuthGuard.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
/**
|
||||
* AuthGuard — 路由守卫
|
||||
* - 未登录 → /login
|
||||
* - 越权(组长访问 /config,非管理员等)→ /dashboard
|
||||
* 扒 banana AuthGuard 范式,全新重建
|
||||
*/
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { useAuthStore } from '@/stores/authStore';
|
||||
import { UserRole } from '@/types/index';
|
||||
|
||||
// 路由 → 最低所需角色
|
||||
const ROUTE_ROLES: Record<string, UserRole[]> = {
|
||||
'/config': ['admin'],
|
||||
'/review': ['admin', 'supervisor'],
|
||||
'/history': ['admin', 'supervisor', 'operator'],
|
||||
'/tasks/new': ['admin', 'operator'],
|
||||
'/dashboard': ['admin', 'supervisor', 'operator'],
|
||||
};
|
||||
|
||||
function getRequiredRoles(pathname: string): UserRole[] | null {
|
||||
// 精确匹配或前缀匹配(如 /tasks/[id]/text)
|
||||
if (ROUTE_ROLES[pathname]) return ROUTE_ROLES[pathname];
|
||||
for (const [prefix, roles] of Object.entries(ROUTE_ROLES)) {
|
||||
if (pathname.startsWith(prefix + '/')) return roles;
|
||||
}
|
||||
return null; // 公开路由(/login 等)
|
||||
}
|
||||
|
||||
interface AuthGuardProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function AuthGuard({ children }: AuthGuardProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { isAuthenticated, role, isLoading, fetchMe, endLoading } = useAuthStore();
|
||||
|
||||
useEffect(() => {
|
||||
// 首屏校验:有 token 且 store 未初始化 → 拉 me;无 token → 结束 loading 让守卫放行判断
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem('clover_token') : null;
|
||||
if (token && !isAuthenticated && isLoading) {
|
||||
fetchMe();
|
||||
} else if (!token && isLoading) {
|
||||
endLoading();
|
||||
}
|
||||
}, [isAuthenticated, isLoading, fetchMe, endLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
|
||||
// 公开路由直接放行
|
||||
if (pathname === '/login') return;
|
||||
|
||||
if (!isAuthenticated) {
|
||||
router.replace('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
const required = getRequiredRoles(pathname);
|
||||
if (required && role && !required.includes(role)) {
|
||||
router.replace('/dashboard');
|
||||
}
|
||||
}, [isAuthenticated, role, isLoading, pathname, router]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen bg-surface-secondary">
|
||||
<div className="text-text-secondary text-sm">加载中…</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
Reference in New Issue
Block a user