feat: Phase 1 - Implementation of Data Security Infrastructure
Implemented comprehensive data security infrastructure for SFERA platform: ## Security Classes Created: - `SupplyDataFilter`: Role-based data filtering for supply orders - `ParticipantIsolation`: Data isolation between competing organizations - `RecipeAccessControl`: Protection of production recipes and trade secrets - `CommercialDataAudit`: Audit logging and suspicious activity detection - `SecurityLogger`: Centralized security event logging system ## Infrastructure Components: - Feature flags system for gradual security rollout - Database migrations for audit logging (AuditLog, SecurityAlert models) - Secure resolver wrapper for automatic GraphQL security - TypeScript interfaces and type safety throughout ## Security Features: - Role-based access control (SELLER, WHOLESALE, FULFILLMENT, LOGIST) - Commercial data protection between competitors - Production recipe confidentiality - Audit trail for all data access - Real-time security monitoring and alerts - Rate limiting and suspicious activity detection ## Implementation Notes: - All console logging replaced with centralized security logger - Comprehensive TypeScript typing with no explicit 'any' types - Modular architecture following SFERA coding standards - Feature flag controlled rollout for safe deployment This completes Phase 1 of the security implementation plan. Next phases will integrate these classes into existing GraphQL resolvers. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -755,6 +755,265 @@ const componentVariants = cva(
|
||||
{loading ? null : <ProductCard data={product} />}
|
||||
```
|
||||
|
||||
## 🏪 КОМПОНЕНТЫ КАБИНЕТА ПОСТАВЩИКА (WHOLESALE)
|
||||
|
||||
### АРХИТЕКТУРА КОМПОНЕНТОВ ПОСТАВЩИКА:
|
||||
|
||||
```typescript
|
||||
src/components/
|
||||
├── warehouse/ # Компоненты склада поставщика
|
||||
│ ├── warehouse-dashboard.tsx # Главный dashboard склада
|
||||
│ ├── product-card.tsx # Карточка товара
|
||||
│ ├── product-form.tsx # Форма создания/редактирования товара
|
||||
│ └── warehouse-statistics.tsx # Статистика склада
|
||||
├── supplier-orders/ # Компоненты обработки заказов
|
||||
│ ├── supplier-orders-dashboard.tsx # Главный dashboard заказов
|
||||
│ ├── supplier-order-card.tsx # Карточка заказа
|
||||
│ ├── supplier-orders-tabs.tsx # Табы по статусам заказов
|
||||
│ ├── supplier-orders-search.tsx # Поиск и фильтры
|
||||
│ └── supplier-order-stats.tsx # Статистика заказов
|
||||
└── economics/ # Экономическая аналитика
|
||||
└── wholesale-economics-page.tsx # Финансовая отчетность
|
||||
```
|
||||
|
||||
### 🏢 КАРТОЧКА ПОСТАВЩИКА В ИНТЕРФЕЙСЕ:
|
||||
|
||||
**Структура карточки:**
|
||||
|
||||
```jsx
|
||||
<div className="supplier-card glass-card">
|
||||
<div className="flex items-start gap-2">
|
||||
{/* Аватар организации */}
|
||||
<OrganizationAvatar organization={supplier} size="sm" />
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Название поставщика */}
|
||||
<h4 className="text-white font-medium text-sm truncate">{supplier.name || supplier.fullName}</h4>
|
||||
|
||||
{/* ИНН и рынок */}
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<p className="text-white/60 text-xs font-mono">ИНН: {supplier.inn}</p>
|
||||
{supplier.market && <Badge className="market-badge">{getMarketLabel(supplier.market)}</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Визуальные правила карточки поставщика:**
|
||||
|
||||
- **Аватар**: Размер `sm`, позиционирование слева от текста
|
||||
- **Название**: Приоритет `name` над `fullName`, с усечением `truncate`
|
||||
- **ИНН**: Моноширинный шрифт `font-mono`, цвет `text-white/60`
|
||||
- **Рынок**: Badge компонент с индивидуальными цветовыми схемами
|
||||
- **Glass эффект**: `glass-card` класс с полупрозрачным фоном
|
||||
|
||||
### 🔍 ПОИСКОВЫЙ ИНТЕРФЕЙС ПОСТАВЩИКОВ:
|
||||
|
||||
```jsx
|
||||
<Input
|
||||
placeholder="Поиск поставщиков..."
|
||||
className="bg-white/5 border-white/10 text-white placeholder:text-white/50 pl-10 h-9"
|
||||
onChange={(e) => handleSupplierSearch(e.target.value)}
|
||||
/>
|
||||
```
|
||||
|
||||
**Особенности поиска:**
|
||||
|
||||
- Glass эффект: `bg-white/5 border-white/10`
|
||||
- Плейсхолдер: `placeholder:text-white/50`
|
||||
- Левый отступ для иконки: `pl-10`
|
||||
- Высота: `h-9` (36px)
|
||||
|
||||
### 🎨 ЦВЕТОВЫЕ СХЕМЫ РЫНКОВ ПОСТАВЩИКОВ:
|
||||
|
||||
```typescript
|
||||
// Примеры цветовых схем для физических рынков
|
||||
const marketColors = {
|
||||
sadovod: 'bg-green-500/20 text-green-300 border-green-500/30',
|
||||
'tyak-moscow': 'bg-blue-500/20 text-blue-300 border-blue-500/30',
|
||||
default: 'bg-gray-500/20 text-gray-300 border-gray-500/30',
|
||||
}
|
||||
|
||||
// Функция получения метки рынка
|
||||
function getMarketLabel(market: string): string {
|
||||
const labels = {
|
||||
sadovod: 'Садовод',
|
||||
'tyak-moscow': 'ТЯК Москва',
|
||||
default: 'Рынок',
|
||||
}
|
||||
return labels[market] || labels.default
|
||||
}
|
||||
```
|
||||
|
||||
### 📦 БЛОКИ ПОСТАВЩИКОВ В СЕЛЛЕР ИНТЕРФЕЙСЕ:
|
||||
|
||||
**Правила горизонтальной прокрутки:**
|
||||
|
||||
```jsx
|
||||
{
|
||||
/* Контейнер с горизонтальной прокруткой */
|
||||
}
|
||||
;<div className="flex gap-3 overflow-x-auto scrollbar-hide pb-2">
|
||||
{suppliers.map((supplier) => (
|
||||
<div
|
||||
key={supplier.id}
|
||||
className="flex-none w-64" // Фиксированная ширина 256px
|
||||
>
|
||||
<SupplierCard supplier={supplier} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Требования к горизонтальным блокам:**
|
||||
|
||||
- Фиксированная ширина карточек: `w-64` (256px)
|
||||
- Отсутствие сжатия: `flex-none`
|
||||
- Скрытие скроллбара: `scrollbar-hide`
|
||||
- Отступ от низа: `pb-2` для визуального комфорта
|
||||
|
||||
### 🚨 CRITICAL UI RULES ДЛЯ ПОСТАВЩИКОВ:
|
||||
|
||||
#### **1. СТАТУСЫ vs КНОПКИ ДЕЙСТВИЙ:**
|
||||
|
||||
```jsx
|
||||
{
|
||||
/* ❌ НЕПРАВИЛЬНО: Показывать статус поставщику */
|
||||
}
|
||||
{
|
||||
user.organization.type === 'WHOLESALE' && <StatusBadge status={order.status}>Ожидает подтверждения</StatusBadge>
|
||||
}
|
||||
|
||||
{
|
||||
/* ✅ ПРАВИЛЬНО: Только кнопки действий для поставщика */
|
||||
}
|
||||
{
|
||||
user.organization.type === 'WHOLESALE' && order.status === 'PENDING' && (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="glass" size="sm" onClick={() => approveOrder(order.id)}>
|
||||
Одобрить
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => rejectOrder(order.id)}>
|
||||
Отклонить
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### **2. ОПЦИОНАЛЬНЫЕ ПОЛЯ УПАКОВКИ ПРИ ОДОБРЕНИИ:**
|
||||
|
||||
```jsx
|
||||
{
|
||||
/* ОПЦИОНАЛЬНЫЕ поля для поставщика - отображаются сразу при одобрении заказа */
|
||||
}
|
||||
;<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="packagesCount">Количество грузовых мест</Label>
|
||||
<Input
|
||||
id="packagesCount"
|
||||
type="number"
|
||||
placeholder="Введите количество (опционально)"
|
||||
aria-describedby="packages-help"
|
||||
/>
|
||||
<p id="packages-help" className="text-xs text-white/60 mt-1">
|
||||
Используется логистикой для расчета тарифов
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="volume">Объем груза (м³)</Label>
|
||||
<Input id="volume" type="number" step="0.01" placeholder="0.00 (опционально)" aria-describedby="volume-help" />
|
||||
<p id="volume-help" className="text-xs text-white/60 mt-1">
|
||||
Помогает логистике в планировании маршрутов
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<Label htmlFor="readyDate">Дата готовности к отгрузке</Label>
|
||||
<GlassDatePicker
|
||||
id="readyDate"
|
||||
value={readyDate}
|
||||
onChange={setReadyDate}
|
||||
placeholder="Выберите дату (опционально)"
|
||||
aria-describedby="ready-date-help"
|
||||
/>
|
||||
<p id="ready-date-help" className="text-xs text-white/60 mt-1">
|
||||
Когда товары будут готовы к передаче логистике
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<Label htmlFor="notes">Комментарии для логистики</Label>
|
||||
<Textarea id="notes" placeholder="Дополнительная информация (опционально)" aria-describedby="notes-help" />
|
||||
<p id="notes-help" className="text-xs text-white/60 mt-1">
|
||||
Особые требования к транспортировке или упаковке
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
/* ВАЖНО: Поля показываются на 1-м уровне визуализации поставки */
|
||||
}
|
||||
;<div className="mt-4">
|
||||
<p className="text-sm text-white/80">ℹ️ Все поля опциональны, но рекомендуются для точного планирования логистики</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### **3. ARIA LABELS ДЛЯ КОМПОНЕНТОВ ПОСТАВЩИКА:**
|
||||
|
||||
```jsx
|
||||
// Кнопки действий с описательными ARIA-атрибутами
|
||||
<Button
|
||||
variant="glass"
|
||||
aria-label={`Одобрить заказ №${order.number} от ${order.organization.name}`}
|
||||
onClick={() => approveOrder(order.id)}
|
||||
>
|
||||
Одобрить
|
||||
</Button>
|
||||
|
||||
// Поля ввода с полными описаниями
|
||||
<Input
|
||||
aria-label="Количество грузовых мест для логистического расчета"
|
||||
aria-required="true"
|
||||
aria-describedby="packages-error packages-help"
|
||||
/>
|
||||
```
|
||||
|
||||
#### **4. СПЕЦИАЛЬНЫЕ РАЗМЕРЫ ДЛЯ КАБИНЕТА ПОСТАВЩИКА:**
|
||||
|
||||
```typescript
|
||||
// Размеры карточек в кабинете поставщика
|
||||
const wholesaleSizes = {
|
||||
supplierCard: 'h-[164px] w-64', // 164px высота, 256px ширина
|
||||
orderCard: 'min-h-[120px]', // Минимум 120px для заказов
|
||||
productCard: 'h-[180px]', // 180px для товарных карточек
|
||||
containerWithPadding: 'h-[196px]', // 164 + 32px отступы сверху/снизу
|
||||
}
|
||||
```
|
||||
|
||||
### 📐 ФОРМУЛА РАСЧЕТА РАЗМЕРОВ КОНТЕЙНЕРОВ:
|
||||
|
||||
```typescript
|
||||
// ОБЯЗАТЕЛЬНАЯ формула для всех контейнеров поставщика
|
||||
const containerHeight = {
|
||||
formula: 'Высота контента + padding-top + padding-bottom',
|
||||
example: {
|
||||
content: '164px', // Высота карточки поставщика
|
||||
paddingTop: '16px',
|
||||
paddingBottom: '16px',
|
||||
totalContainer: '196px' // 164 + 16 + 16 = 196px
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ ЗАПРЕЩЕНО: Произвольные размеры без расчета
|
||||
<div className="h-200"> {/* Откуда 200px? */}
|
||||
|
||||
// ✅ ПРАВИЛЬНО: С математическим обоснованием
|
||||
<div className="h-[196px]"> {/* 164px + 32px отступы */}
|
||||
```
|
||||
|
||||
## 📱 АДАПТИВНОСТЬ
|
||||
|
||||
### Responsive Breakpoints:
|
||||
|
Reference in New Issue
Block a user