← Назад к вопросам
Как делать разные отчеты для разных ролей?
1.3 Junior🔥 151 комментариев
#Soft Skills и рабочие процессы
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Управление отчетами для разных ролей пользователей
Создание разных отчетов для разных ролей требует контролируемого доступа как на фронтенде, так и на бэкенде. Это многоуровневый подход, объединяющий авторизацию, условную логику и фильтрацию данных.
Архитектурный подход
Frontend отвечает за:
- Отображение доступных отчетов в UI
- Скрытие компонентов, к которым нет доступа
- Улучшенный UX для каждой роли
Backend отвечает за:
- Валидацию разрешений перед отправкой данных
- Фильтрацию данных согласно роли
- Безопасность (главное!)
1. Role-Based Access Control (RBAC)
// contexts/AuthContext.ts
interface User {
id: string;
name: string;
role: 'admin' | 'manager' | 'analyst' | 'viewer';
permissions: Permission[];
}
type Permission = 'view_reports' | 'create_reports' | 'delete_reports' | 'export_reports';
interface AuthContextType {
user: User | null;
hasPermission: (permission: Permission) => boolean;
hasRole: (role: User['role']) => boolean;
}
export const AuthContext = createContext<AuthContextType>({
user: null,
hasPermission: () => false,
hasRole: () => false,
});
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const hasPermission = useCallback((permission: Permission) => {
return user?.permissions.includes(permission) ?? false;
}, [user]);
const hasRole = useCallback((role: User['role']) => {
return user?.role === role;
}, [user]);
return (
<AuthContext.Provider value={{ user, hasPermission, hasRole }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
2. Условная визуализация компонентов
// components/reports/ReportList.tsx
export function ReportList() {
const { user, hasPermission, hasRole } = useAuth();
const [reports, setReports] = useState<Report[]>([]);
useEffect(() => {
if (hasPermission('view_reports')) {
fetchReports(user?.role);
}
}, [hasPermission, user?.role]);
if (!hasPermission('view_reports')) {
return <AccessDenied />;
}
return (
<div>
<div className="report-filters">
{/* Только менеджеры и админы видят фильтры по подразделениям */}
{hasRole('manager') || hasRole('admin') && (
<DepartmentFilter />
)}
</div>
<div className="report-table">
{reports.map(report => (
<ReportRow key={report.id} report={report}>
{/* Кнопка удаления только для админов */}
{hasRole('admin') && (
<DeleteButton reportId={report.id} />
)}
{/* Экспорт для менеджеров и выше */}
{(hasRole('admin') || hasRole('manager')) && (
<ExportButton report={report} />
)}
</ReportRow>
))}
</div>
</div>
);
}
3. Backend API с фильтрацией по ролям
# Backend (FastAPI)
from enum import Enum
from sqlalchemy.orm import Session
from fastapi import Depends, HTTPException
class UserRole(str, Enum):
ADMIN = "admin"
MANAGER = "manager"
ANALYST = "analyst"
VIEWER = "viewer"
@app.get("/api/v1/reports")
async def get_reports(
user: User = Depends(get_current_user),
department_id: str | None = None,
db: Session = Depends(get_db)
):
# Базовая проверка доступа
if "view_reports" not in user.permissions:
raise HTTPException(status_code=403, detail="Access denied")
query = db.query(Report)
# Фильтрация по ролям
if user.role == UserRole.VIEWER:
# Viewer видит только опубликованные отчеты
query = query.filter(Report.is_published == True)
elif user.role == UserRole.ANALYST:
# Analyst видит свои отчеты и опубликованные
query = query.filter(
(Report.created_by == user.id) | (Report.is_published == True)
)
elif user.role == UserRole.MANAGER:
# Manager видит отчеты своего подразделения
query = query.filter(Report.department_id == user.department_id)
# Admin видит ВСЕ отчеты (фильтра нет)
# Дополнительная фильтрация по department_id (если позволяет роль)
if department_id and user.role in (UserRole.ADMIN, UserRole.MANAGER):
if user.role == UserRole.MANAGER and department_id != user.department_id:
raise HTTPException(status_code=403, detail="Cannot access other departments")
query = query.filter(Report.department_id == department_id)
reports = query.all()
return reports
4. Кастомный хук для управления доступом
// hooks/useReportAccess.ts
interface ReportAccessConfig {
admin: boolean;
manager: boolean;
analyst: boolean;
viewer: boolean;
}
export function useReportAccess() {
const { user, hasRole } = useAuth();
const canViewReport = (report: Report): boolean => {
if (hasRole('admin')) return true;
if (hasRole('manager')) {
return report.departmentId === user?.departmentId || report.isPublished;
}
if (hasRole('analyst')) {
return report.createdBy === user?.id || report.isPublished;
}
if (hasRole('viewer')) {
return report.isPublished;
}
return false;
};
const canEditReport = (report: Report): boolean => {
if (hasRole('admin')) return true;
if (hasRole('manager')) {
return report.departmentId === user?.departmentId;
}
if (hasRole('analyst')) {
return report.createdBy === user?.id;
}
return false;
};
const canDeleteReport = (report: Report): boolean => {
return hasRole('admin') || (hasRole('analyst') && report.createdBy === user?.id);
};
const canExportReport = (): boolean => {
return hasRole('admin') || hasRole('manager') || hasRole('analyst');
};
return {
canViewReport,
canEditReport,
canDeleteReport,
canExportReport,
};
}
5. Компонент с контролем доступа
// components/reports/ReportDetail.tsx
interface ReportDetailProps {
reportId: string;
}
export function ReportDetail({ reportId }: ReportDetailProps) {
const { user } = useAuth();
const { canViewReport, canEditReport, canDeleteReport, canExportReport } = useReportAccess();
const [report, setReport] = useState<Report | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetchReport(reportId)
.then(data => {
// Проверка доступа на фронтенде
if (!canViewReport(data)) {
throw new Error('Access denied');
}
setReport(data);
})
.catch(err => setError(err))
.finally(() => setLoading(false));
}, [reportId, canViewReport]);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!report) return null;
return (
<div className="report-detail">
<div className="report-header">
<h1>{report.title}</h1>
<div className="report-actions">
{canEditReport(report) && (
<button onClick={() => openEditor(report.id)}>Edit</button>
)}
{canDeleteReport(report) && (
<button onClick={() => deleteReport(report.id)}>Delete</button>
)}
{canExportReport() && (
<button onClick={() => exportReport(report)}>Export</button>
)}
</div>
</div>
<div className="report-content">
{/* Скрытие чувствительных данных для зрителей */}
{(user?.role === 'admin' || user?.role === 'manager' || user?.role === 'analyst') && (
<div className="sensitive-metrics">
<p>Revenue: ${report.revenue}</p>
<p>Cost: ${report.cost}</p>
</div>
)}
{/* Доступно для всех */}
<ReportChart data={report.data} />
</div>
</div>
);
}
6. Матрица доступа (reference table)
// types/rbac.ts
const ROLE_PERMISSIONS: Record<UserRole, Permission[]> = {
admin: [
'view_reports',
'create_reports',
'edit_all_reports',
'delete_all_reports',
'export_reports',
'manage_users',
'view_all_data',
],
manager: [
'view_reports',
'create_reports',
'edit_department_reports',
'export_reports',
'view_department_data',
],
analyst: [
'view_reports',
'create_reports',
'edit_own_reports',
'export_reports',
],
viewer: [
'view_published_reports',
],
};
7. Protected Route компонент
// components/ProtectedRoute.tsx
interface ProtectedRouteProps {
children: React.ReactNode;
requiredRole?: UserRole | UserRole[];
requiredPermission?: Permission;
}
export function ProtectedRoute({
children,
requiredRole,
requiredPermission,
}: ProtectedRouteProps) {
const { hasRole, hasPermission } = useAuth();
const hasAccess = (() => {
if (requiredRole) {
const roles = Array.isArray(requiredRole) ? requiredRole : [requiredRole];
return roles.some(role => hasRole(role));
}
if (requiredPermission) {
return hasPermission(requiredPermission);
}
return true;
})();
if (!hasAccess) {
return <AccessDenied />;
}
return <>{children}</>;
}
// Использование
<ProtectedRoute requiredRole={['admin', 'manager']}>
<AdvancedReportBuilder />
</ProtectedRoute>
Best Practices
1. Принцип наименьших привилегий
- Давай минимальные права по умолчанию
- Добавляй специфичные разрешения по необходимости
2. Не полагайся только на фронтенд
- Backend ДОЛЖЕН проверять разрешения
- Фронтенд скрывает UI, но не обеспечивает безопасность
3. Логирование доступа
// На бэкенде
db.log_access(user_id, 'view_report', report_id, timestamp)
4. Кэширование прав
const cachedPermissions = useMemo(() => {
return ROLE_PERMISSIONS[user.role];
}, [user.role]);
Итог
Управление отчетами для разных ролей требует:
- Backend валидация (главное!)
- Frontend фильтрация (UX)
- Матрица доступа (контроль)
- Защищённые routes (безопасность)
Это обеспечит безопасность и правильную работу приложения.