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:
@ -548,6 +548,242 @@ type Organization {
|
||||
referralCode: String
|
||||
referralPoints: Int!
|
||||
|
||||
# Marketplace данные
|
||||
market: String # Физический рынок (для WHOLESALE)
|
||||
# Временные метки
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
```
|
||||
|
||||
## 🏪 СПЕЦИФИЧНЫЕ ПРАВИЛА ДЛЯ ПОСТАВЩИКОВ (WHOLESALE)
|
||||
|
||||
### ЗАПРОСЫ ПОСТАВЩИКОВ:
|
||||
|
||||
```graphql
|
||||
# Получение товаров поставщика
|
||||
query GetMyProducts {
|
||||
myProducts {
|
||||
id
|
||||
name
|
||||
article
|
||||
price
|
||||
quantity
|
||||
organization {
|
||||
id
|
||||
name
|
||||
market # Физический рынок поставщика
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Получение входящих заказов поставщика
|
||||
query GetSupplierOrders($status: SupplyOrderStatus) {
|
||||
supplyOrders(where: { partnerId: $myOrgId, status: $status }) {
|
||||
id
|
||||
status
|
||||
totalAmount
|
||||
deliveryDate
|
||||
organization {
|
||||
name
|
||||
inn
|
||||
} # Заказчик
|
||||
fulfillmentCenter {
|
||||
name
|
||||
address
|
||||
} # Получатель
|
||||
items {
|
||||
id
|
||||
quantity
|
||||
price
|
||||
totalPrice
|
||||
product {
|
||||
id
|
||||
name
|
||||
article
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Получение партнеров поставщика
|
||||
query GetMyCounterparties($type: OrganizationType) {
|
||||
myCounterparties(type: $type) {
|
||||
id
|
||||
name
|
||||
type
|
||||
market
|
||||
fullName
|
||||
inn
|
||||
isCounterparty
|
||||
hasOutgoingRequest
|
||||
hasIncomingRequest
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### МУТАЦИИ ПОСТАВЩИКОВ:
|
||||
|
||||
```graphql
|
||||
# Одобрение заказа поставщиком с опциональными полями упаковки
|
||||
mutation SupplierApproveOrder(
|
||||
$orderId: ID!
|
||||
$packagesCount: Int
|
||||
$volume: Float
|
||||
$readyDate: DateTime
|
||||
$notes: String
|
||||
) {
|
||||
supplierApproveOrder(
|
||||
id: $orderId
|
||||
packagesCount: $packagesCount # Опционально: для логистических расчетов
|
||||
volume: $volume # Опционально: для планирования логистики
|
||||
readyDate: $readyDate # Опционально: дата готовности к отгрузке
|
||||
notes: $notes # Опционально: комментарии
|
||||
) {
|
||||
success
|
||||
message
|
||||
order {
|
||||
id
|
||||
status # PENDING → SUPPLIER_APPROVED
|
||||
organization {
|
||||
id
|
||||
name
|
||||
}
|
||||
totalAmount
|
||||
packagesCount # null если не указано
|
||||
volume # null если не указано
|
||||
readyDate # null если не указано
|
||||
notes # null если не указано
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Отклонение заказа поставщиком
|
||||
mutation SupplierRejectOrder($orderId: ID!, $reason: String) {
|
||||
supplierRejectOrder(id: $orderId, reason: $reason) {
|
||||
success
|
||||
message
|
||||
order {
|
||||
id
|
||||
status # PENDING → CANCELLED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Отгрузка товара поставщиком
|
||||
mutation SupplierShipOrder($orderId: ID!) {
|
||||
supplierShipOrder(id: $orderId) {
|
||||
success
|
||||
message
|
||||
order {
|
||||
id
|
||||
status # LOGISTICS_CONFIRMED → SHIPPED
|
||||
organization {
|
||||
id
|
||||
name
|
||||
}
|
||||
logisticsPartner {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Создание товара поставщиком
|
||||
mutation CreateProduct($input: ProductInput!) {
|
||||
createProduct(input: $input) {
|
||||
success
|
||||
message
|
||||
product {
|
||||
id
|
||||
article
|
||||
name
|
||||
price
|
||||
organization {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ПРАВИЛА АВТОРИЗАЦИИ ПОСТАВЩИКОВ:
|
||||
|
||||
```typescript
|
||||
// Resolver-level security для поставщиков
|
||||
const wholesaleResolvers = {
|
||||
// Проверка что пользователь - поставщик
|
||||
validateWholesaleAccess: (context) => {
|
||||
if (context.user.organization.type !== 'WHOLESALE') {
|
||||
throw new GraphQLError('Access denied: Wholesale access required')
|
||||
}
|
||||
},
|
||||
|
||||
// Фильтрация заказов для поставщика
|
||||
getSupplierOrders: async (parent, args, context) => {
|
||||
// Поставщик видит только заказы где он является поставщиком
|
||||
return await prisma.supplyOrder.findMany({
|
||||
where: {
|
||||
partnerId: context.user.organization.id, // Мы - поставщик
|
||||
...args.where,
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
// Проверка доступа к товарам
|
||||
validateProductAccess: async (productId, context) => {
|
||||
const product = await prisma.product.findFirst({
|
||||
where: {
|
||||
id: productId,
|
||||
organizationId: context.user.organizationId, // Только свои товары
|
||||
},
|
||||
})
|
||||
|
||||
if (!product) {
|
||||
throw new GraphQLError('Product not found or access denied')
|
||||
}
|
||||
return product
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### КРИТИЧЕСКИЕ ПРАВИЛА ПАРТНЕРСТВА:
|
||||
|
||||
```typescript
|
||||
// ✅ ПРАВИЛЬНО: Поставщики берутся ТОЛЬКО из партнеров
|
||||
const getWholesalePartners = `
|
||||
query GetMyCounterparties {
|
||||
myCounterparties(type: WHOLESALE) {
|
||||
id, name, fullName, inn, market
|
||||
isCounterparty # Должно быть true
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// ❌ НЕПРАВИЛЬНО: Прямой запрос всех поставщиков
|
||||
const wrongSupplierQuery = `
|
||||
query GetAllSuppliers {
|
||||
organizations(type: WHOLESALE) { # Неправильно - нет проверки партнерства
|
||||
id, name
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Правильная фильтрация в резолвере:
|
||||
const correctPartnershipFilter = `
|
||||
// Показываем только организации-партнеры
|
||||
const counterparties = await prisma.counterparty.findMany({
|
||||
where: {
|
||||
initiatorId: currentUser.organization.id,
|
||||
status: 'ACCEPTED',
|
||||
partner: { type: 'WHOLESALE' }
|
||||
},
|
||||
include: { partner: true }
|
||||
})
|
||||
`;
|
||||
|
||||
# Временные метки (обязательно)
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
|
Reference in New Issue
Block a user