Дата: 2025-11-10
Версия: 1.0.0
Цель: Каскадная система абстракций, тем и безопасности
1. DATA ABSTRACTION LAYERS (Уровни абстракции данных)
Database → ORM → DTO → ViewModel → API Response
2. THEME SYSTEM (Система тем)
Base Theme → Design Tokens → Theme Variants → Theme Builder
3. DESIGN SYSTEM (Система дизайна)
Primitives → Components → Patterns → Templates
4. SECURITY (Безопасность)
OAuth 2.0 → OpenID Connect → RBAC → RLS → Field-level
5. CASCADE SYSTEM (Каскадность)
Global → Theme → Component → Element → Inline
┌─────────────────────────────────────────────────────────┐
│ УРОВЕНЬ 1: DATABASE (База данных) │
│ Сырые данные в таблицах │
├─────────────────────────────────────────────────────────┤
│ users table: │
│ | id | email | password_hash | created_at | │
│ | 1 | user@test.com | $2b$12... | 2025-11-10 | │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ УРОВЕНЬ 2: ORM MODEL (Объектная модель) │
│ SQLAlchemy / Django ORM │
├─────────────────────────────────────────────────────────┤
│ class User(Base): │
│ id = Column(UUID, primary_key=True) │
│ email = Column(String, unique=True) │
│ password_hash = Column(String) │
│ created_at = Column(DateTime) │
│ # Relationships │
│ orders = relationship('Order') │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ УРОВЕНЬ 3: DTO (Data Transfer Object) │
│ Чистые данные для передачи между слоями │
├─────────────────────────────────────────────────────────┤
│ @dataclass │
│ class UserDTO: │
│ id: UUID │
│ email: str │
│ full_name: str │
│ created_at: datetime │
│ # БЕЗ password_hash (безопасность!) │
│ # БЕЗ relationships (производительность!) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ УРОВЕНЬ 4: VIEW MODEL (Модель представления) │
│ Данные + логика для конкретного view │
├─────────────────────────────────────────────────────────┤
│ @dataclass │
│ class UserProfileViewModel: │
│ user: UserDTO │
│ orders_count: int │
│ total_spent: Decimal │
│ last_login_ago: str # "3 дня назад" │
│ avatar_url: str │
│ is_vip: bool │
│ │
│ # View logic │
│ def display_name(self) -> str: │
│ return self.user.full_name or self.user.email │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ УРОВЕНЬ 5: API RESPONSE (API ответ) │
│ JSON для клиента │
├─────────────────────────────────────────────────────────┤
│ { │
│ "id": "123e4567-e89b-12d3-a456-426614174000", │
│ "email": "user@test.com", │
│ "full_name": "Иван Петров", │
│ "stats": { │
│ "orders_count": 42, │
│ "total_spent": "150000.00", │
│ "last_login_ago": "3 дня назад" │
│ }, │
│ "avatar": "https://cdn.example.com/avatars/123.jpg",│
│ "badges": ["vip", "verified"] │
│ } │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ УРОВЕНЬ 6: UI COMPONENT (UI компонент) │
│ Визуализация для пользователя │
├─────────────────────────────────────────────────────────┤
│ <UserProfileCard> │
│ <Avatar src={user.avatar} /> │
│ <h2>{user.full_name}</h2> │
│ <p>{user.email}</p> │
│ <Stats> │
│ <Stat label="Заказов" value={orders_count} /> │
│ <Stat label="Потрачено" value={total_spent} /> │
│ </Stats> │
│ {user.is_vip && <VIPBadge />} │
│ </UserProfileCard> │
└─────────────────────────────────────────────────────────┘
# entities/user.cifra
entity:
name: User
table: users
# УРОВЕНЬ 2: ORM Model
fields:
email: {type: email, unique: true}
password_hash: {type: string, hidden: true}
full_name: {type: string}
created_at: {type: datetime, auto: true}
# УРОВЕНЬ 3: DTO
dto:
fields: [id, email, full_name, created_at]
exclude: [password_hash] # Никогда не передаём наружу
# УРОВЕНЬ 4: View Models
view_models:
profile:
extends: dto
computed:
- orders_count: "COUNT(orders)"
- total_spent: "SUM(orders.total)"
- last_login_ago: "format_date_ago(last_login_at)"
- avatar_url: "get_avatar_url(id)"
- is_vip: "total_spent > 100000"
list_item:
fields: [id, email, full_name, created_at]
computed:
- display_name: "full_name OR email"
# УРОВЕНЬ 5: API Response
api:
endpoints:
- path: /users/me
method: GET
response: profile # Использует view model
auth: required
- path: /users
method: GET
response: list_item[]
auth: required
permissions: [users:read]
# УРОВЕНЬ 6: UI Components
ui:
components:
- profile_card:
template: UserProfileCard
data: profile
Концепция: Минималистичная тема БЕЗ стилизации - только структура
# themes/base/base.theme.cifra
theme:
id: base
name: "Base Theme"
description: "Голая тема - только структура, без стилей"
version: 1.0.0
# DESIGN TOKENS (токены дизайна)
tokens:
# Цвета (переменные)
colors:
primary: var(--color-primary)
secondary: var(--color-secondary)
success: var(--color-success)
warning: var(--color-warning)
error: var(--color-error)
background: var(--color-background)
surface: var(--color-surface)
text:
primary: var(--color-text-primary)
secondary: var(--color-text-secondary)
# Размеры
spacing:
unit: 4px # Базовая единица
xs: calc(var(--spacing-unit) * 1) # 4px
sm: calc(var(--spacing-unit) * 2) # 8px
md: calc(var(--spacing-unit) * 4) # 16px
lg: calc(var(--spacing-unit) * 6) # 24px
xl: calc(var(--spacing-unit) * 8) # 32px
# Типографика
typography:
font_family: var(--font-family)
font_size:
xs: 12px
sm: 14px
base: 16px
lg: 18px
xl: 20px
"2xl": 24px
"3xl": 30px
font_weight:
normal: 400
medium: 500
semibold: 600
bold: 700
line_height:
tight: 1.25
normal: 1.5
relaxed: 1.75
# Радиусы скругления
radius:
none: 0
sm: 4px
md: 6px
lg: 8px
xl: 12px
full: 9999px
# Тени
shadows:
none: none
sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05)
md: 0 4px 6px -1px rgba(0, 0, 0, 0.1)
lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1)
xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1)
# Переходы
transitions:
fast: 150ms
normal: 250ms
slow: 400ms
easing: cubic-bezier(0.4, 0, 0.2, 1)
# Breakpoints (адаптивность)
breakpoints:
xs: 375px
sm: 640px
md: 768px
lg: 1024px
xl: 1280px
"2xl": 1536px
# LAYOUT (раскладка)
layout:
type: blank # Без предустановленной раскладки
regions: [] # Регионы определяются в дочерних темах
# COMPONENTS (компоненты без стилей)
components:
button:
class: "btn"
variants: [primary, secondary, success, danger]
sizes: [sm, md, lg]
# Без конкретных стилей - только классы
input:
class: "input"
variants: [default, error]
sizes: [sm, md, lg]
card:
class: "card"
variants: [default, bordered, elevated]
# themes/admin_light.theme.cifra
theme:
id: admin_light
name: "Admin Light"
extends: base # Наследуем Base Theme
# Переопределяем токены
tokens:
colors:
primary: "#3B82F6"
secondary: "#64748B"
success: "#10B981"
warning: "#F59E0B"
error: "#EF4444"
background: "#F8FAFC"
surface: "#FFFFFF"
text:
primary: "#1E293B"
secondary: "#64748B"
typography:
font_family: "'Inter', system-ui, sans-serif"
# Добавляем раскладку
layout:
type: sidebar_left
regions:
- sidebar:
width: 280px
background: surface
border_right: 1px solid #E2E8F0
- topbar:
height: 64px
background: surface
border_bottom: 1px solid #E2E8F0
- content:
padding: md
background: background
# Переопределяем компоненты
components:
button:
extends: base.button
styles:
default:
padding: "8px 16px"
border_radius: radius.md
font_weight: font_weight.medium
transition: all transitions.normal transitions.easing
variants:
primary:
background: colors.primary
color: white
hover:
background: darken(colors.primary, 10%)
secondary:
background: colors.secondary
color: white
# theme_builder.cifra
theme_builder:
# Визуальный редактор тем
editor:
sections:
# 1. Цвета
- colors:
primary:
type: color_picker
default: "#3B82F6"
secondary:
type: color_picker
default: "#64748B"
# Авто-генерация оттенков
generate_shades: true # 50, 100, 200... 900
# 2. Типографика
- typography:
font_family:
type: font_select
options: [Inter, Roboto, "Open Sans", ...]
scale:
type: slider
min: 0.8
max: 1.2
step: 0.1
# 3. Размеры
- spacing:
unit:
type: number
default: 4
unit: px
scale:
type: select
options: [linear, geometric, fibonacci]
# 4. Компоненты
- components:
button:
border_radius:
type: slider
min: 0
max: 24
unit: px
shadow:
type: select
options: [none, sm, md, lg]
# Live preview
preview:
enabled: true
screens:
- desktop: 1440px
- tablet: 768px
- mobile: 375px
# Export
export:
formats:
- css_variables # :root { --color-primary: #3B82F6; }
- scss_variables # $color-primary: #3B82F6;
- js_object # { colors: { primary: '#3B82F6' } }
- cifra_theme # theme.cifra файл
Из: Design Tokens Community Group (W3C)
{
"color": {
"brand": {
"primary": {
"value": "#3B82F6",
"type": "color"
},
"secondary": {
"value": "#64748B",
"type": "color"
}
},
"semantic": {
"success": {
"value": "{color.green.500}",
"type": "color"
},
"error": {
"value": "{color.red.500}",
"type": "color"
}
}
},
"spacing": {
"scale": {
"0": {"value": "0", "type": "dimension"},
"1": {"value": "4px", "type": "dimension"},
"2": {"value": "8px", "type": "dimension"},
"4": {"value": "16px", "type": "dimension"}
}
},
"typography": {
"heading": {
"h1": {
"fontSize": {"value": "{font.size.3xl}"},
"fontWeight": {"value": "{font.weight.bold}"},
"lineHeight": {"value": "{line.height.tight}"}
}
}
}
}
# Каскад стилей (от общего к частному)
# УРОВЕНЬ 1: Global (глобальные стили)
global:
colors:
primary: "#3B82F6"
typography:
font_family: "Inter"
↓ наследуется
# УРОВЕНЬ 2: Theme (тема)
theme: admin_light
colors:
primary: "#1E40AF" # Переопределяет global
↓ наследуется
# УРОВЕНЬ 3: Component (компонент)
component: Button
colors:
primary: "#2563EB" # Переопределяет theme
↓ наследуется
# УРОВЕНЬ 4: Variant (вариант)
variant: button.large
padding: "12px 24px" # Дополняет component
↓ наследуется
# УРОВЕНЬ 5: State (состояние)
state: button:hover
colors:
primary: "#1D4ED8" # Переопределяет при наведении
↓ наследуется
# УРОВЕНЬ 6: Inline (inline стили)
inline:
colors:
primary: "#1E3A8A" # Наивысший приоритет
# cifra/core/cascade.py
class CascadeResolver:
"""
Разрешает каскад стилей (как в CSS)
"""
def __init__(self):
self.layers = [
'global', # Приоритет 1 (низкий)
'theme', # Приоритет 2
'component', # Приоритет 3
'variant', # Приоритет 4
'state', # Приоритет 5
'inline' # Приоритет 6 (высокий)
]
def resolve(self, property: str, context: dict) -> Any:
"""
Разрешает значение свойства с учётом каскада
Args:
property: "colors.primary" или "typography.font_family"
context: {
'global': {...},
'theme': {...},
'component': {...},
'variant': {...},
'state': {...},
'inline': {...}
}
Returns:
Значение с наивысшим приоритетом
"""
value = None
# Идём от низкого приоритета к высокому
for layer in self.layers:
layer_value = self._get_nested(context.get(layer, {}), property)
if layer_value is not None:
value = layer_value
return value
def _get_nested(self, obj: dict, path: str) -> Any:
"""Получить вложенное свойство: colors.primary"""
keys = path.split('.')
for key in keys:
if isinstance(obj, dict):
obj = obj.get(key)
else:
return None
return obj
# Использование:
resolver = CascadeResolver()
context = {
'global': {
'colors': {'primary': '#3B82F6'}
},
'theme': {
'colors': {'primary': '#1E40AF'} # Переопределяет
},
'component': {
'colors': {'primary': '#2563EB'} # Переопределяет ещё раз
}
}
color = resolver.resolve('colors.primary', context)
# → '#2563EB' (из component, т.к. высший приоритет)
# security/oauth.cifra
security:
authentication:
# Локальная аутентификация
local:
enabled: true
password:
min_length: 8
require_uppercase: true
require_lowercase: true
require_numbers: true
require_special: false
# OAuth 2.0 провайдеры
oauth:
# Google
- provider: google
enabled: true
client_id: "{env.GOOGLE_CLIENT_ID}"
client_secret: "{env.GOOGLE_CLIENT_SECRET}"
authorization_endpoint: https://accounts.google.com/o/oauth2/v2/auth
token_endpoint: https://oauth2.googleapis.com/token
userinfo_endpoint: https://www.googleapis.com/oauth2/v3/userinfo
scopes: [openid, email, profile]
button_text: "Войти через Google"
button_icon: google
# GitHub
- provider: github
enabled: true
client_id: "{env.GITHUB_CLIENT_ID}"
client_secret: "{env.GITHUB_CLIENT_SECRET}"
authorization_endpoint: https://github.com/login/oauth/authorize
token_endpoint: https://github.com/login/oauth/access_token
userinfo_endpoint: https://api.github.com/user
scopes: [read:user, user:email]
button_text: "Войти через GitHub"
# Microsoft (Azure AD)
- provider: microsoft
enabled: true
tenant: common # или конкретный tenant ID
client_id: "{env.MICROSOFT_CLIENT_ID}"
client_secret: "{env.MICROSOFT_CLIENT_SECRET}"
authorization_endpoint: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
token_endpoint: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
scopes: [openid, email, profile]
# Custom OAuth provider
- provider: custom_sso
enabled: true
name: "Company SSO"
client_id: "{env.SSO_CLIENT_ID}"
client_secret: "{env.SSO_CLIENT_SECRET}"
authorization_endpoint: https://sso.company.com/oauth/authorize
token_endpoint: https://sso.company.com/oauth/token
userinfo_endpoint: https://sso.company.com/oauth/userinfo
scopes: [openid, email, profile]
# OpenID Connect
openid_connect:
enabled: true
discovery_url: https://auth.example.com/.well-known/openid-configuration
# Автоматически загружает endpoints из discovery
# JWT настройки
jwt:
secret: "{env.JWT_SECRET}"
algorithm: HS256
access_token_expire: 3600 # 1 час
refresh_token_expire: 2592000 # 30 дней
# Session
session:
provider: redis
cookie_name: cifra_session
cookie_secure: true
cookie_httponly: true
cookie_samesite: lax
# cifra/security/oauth.py
from authlib.integrations.httpx_client import AsyncOAuth2Client
class OAuthProvider:
"""Провайдер OAuth 2.0"""
def __init__(self, config: dict):
self.provider_name = config['provider']
self.client_id = config['client_id']
self.client_secret = config['client_secret']
self.authorization_endpoint = config['authorization_endpoint']
self.token_endpoint = config['token_endpoint']
self.userinfo_endpoint = config['userinfo_endpoint']
self.scopes = config.get('scopes', [])
async def get_authorization_url(self, redirect_uri: str, state: str) -> str:
"""
Получить URL для редиректа пользователя
Returns:
https://accounts.google.com/o/oauth2/v2/auth?
client_id=...&
redirect_uri=...&
scope=openid email profile&
state=random_state&
response_type=code
"""
client = AsyncOAuth2Client(
client_id=self.client_id,
redirect_uri=redirect_uri,
scope=' '.join(self.scopes)
)
auth_url, state = client.create_authorization_url(
self.authorization_endpoint,
state=state
)
return auth_url
async def exchange_code_for_token(
self,
code: str,
redirect_uri: str
) -> dict:
"""
Обменять authorization code на access token
Returns:
{
'access_token': '...',
'token_type': 'Bearer',
'expires_in': 3600,
'refresh_token': '...',
'id_token': '...' # если OpenID Connect
}
"""
client = AsyncOAuth2Client(
client_id=self.client_id,
client_secret=self.client_secret,
redirect_uri=redirect_uri
)
token = await client.fetch_token(
self.token_endpoint,
code=code
)
return token
async def get_user_info(self, access_token: str) -> dict:
"""
Получить информацию о пользователе
Returns:
{
'sub': 'google_user_id',
'email': 'user@gmail.com',
'name': 'John Doe',
'picture': 'https://...'
}
"""
client = AsyncOAuth2Client(token={'access_token': access_token})
response = await client.get(self.userinfo_endpoint)
return response.json()
# FastAPI integration
from fastapi import APIRouter, HTTPException, Depends
from fastapi.responses import RedirectResponse
router = APIRouter()
@router.get('/auth/{provider}/login')
async def oauth_login(provider: str):
"""Инициировать OAuth flow"""
oauth_provider = get_oauth_provider(provider)
# Генерируем random state для защиты от CSRF
state = generate_random_state()
save_state_to_session(state)
# Получаем authorization URL
redirect_uri = f"{settings.BASE_URL}/auth/{provider}/callback"
auth_url = await oauth_provider.get_authorization_url(
redirect_uri=redirect_uri,
state=state
)
return RedirectResponse(auth_url)
@router.get('/auth/{provider}/callback')
async def oauth_callback(
provider: str,
code: str,
state: str
):
"""Callback после успешной авторизации"""
# Проверяем state (защита от CSRF)
if not verify_state(state):
raise HTTPException(400, "Invalid state")
oauth_provider = get_oauth_provider(provider)
# Обмениваем code на token
redirect_uri = f"{settings.BASE_URL}/auth/{provider}/callback"
token = await oauth_provider.exchange_code_for_token(
code=code,
redirect_uri=redirect_uri
)
# Получаем информацию о пользователе
user_info = await oauth_provider.get_user_info(
access_token=token['access_token']
)
# Создаём или обновляем пользователя
user = await create_or_update_user_from_oauth(
provider=provider,
provider_user_id=user_info['sub'],
email=user_info['email'],
name=user_info.get('name'),
avatar=user_info.get('picture')
)
# Создаём JWT токен для нашей системы
jwt_token = create_jwt_token(user)
# Редиректим на фронтенд с токеном
return RedirectResponse(
f"{settings.FRONTEND_URL}?token={jwt_token}"
)
security:
# Role-Based Access Control
rbac:
roles:
- admin: {permissions: ["*"]}
- manager: {permissions: [contacts:*, deals:*]}
- user: {permissions: [contacts:read, deals:read:own]}
# Row-Level Security
rls:
Contact:
- policy: own_contacts
condition: "owner_id == current_user.id"
roles: [user]
# Field-Level Security
field_security:
Contact:
phone:
read: [admin, manager]
write: [admin]
Концепция из Drupal: Данные для рендеринга в виде массивов
# cifra/core/render.py
def render_user_profile(user):
"""
Возвращает render array (не HTML!)
Позволяет другим модулям изменить данные до рендеринга
"""
render_array = {
'#theme': 'user_profile',
'#user': user,
'#cache': {
'keys': ['user', user.id],
'tags': ['user:' + str(user.id)],
'max_age': 3600
},
'#attributes': {
'class': ['user-profile'],
'data-user-id': user.id
},
'avatar': {
'#theme': 'image',
'#uri': user.avatar_url,
'#alt': user.full_name,
'#attributes': {'class': ['user-avatar']}
},
'name': {
'#markup': f'<h2>{user.full_name}</h2>',
'#weight': 10
},
'email': {
'#markup': f'<p>{user.email}</p>',
'#weight': 20
}
}
# Позволяем другим модулям изменить
alter_hooks.invoke_all('user_profile_alter', render_array, user)
return render_array
# cifra/themes/hooks.py
def hook_theme():
"""
Регистрирует все theme hooks
Аналог hook_theme() в Drupal
"""
return {
'user_profile': {
'template': 'user-profile',
'variables': {
'user': None,
'show_avatar': True,
'show_stats': True
}
},
'button': {
'template': 'button',
'variables': {
'label': '',
'type': 'button',
'variant': 'primary',
'size': 'md',
'disabled': False
}
}
}
def theme_user_profile(variables):
"""
Рендерит user profile
Может быть переопределена в дочерней теме
"""
user = variables['user']
show_avatar = variables['show_avatar']
return f"""
<div class="user-profile">
{f'<img src="{user.avatar}" />' if show_avatar else ''}
<h2>{user.full_name}</h2>
<p>{user.email}</p>
</div>
"""
# cifra/themes/preprocess.py
def preprocess_user_profile(variables):
"""
Предобработка перед рендерингом
Добавляет дополнительные переменные
"""
user = variables['user']
# Добавляем вычисляемые поля
variables['display_name'] = user.full_name or user.email
variables['member_since'] = format_date(user.created_at)
variables['is_vip'] = user.total_spent > 100000
# Добавляем CSS классы
variables['attributes']['class'].append('user-type-' + user.role)
if variables['is_vip']:
variables['attributes']['class'].append('user-vip')
def preprocess_button(variables):
"""Предобработка кнопки"""
# Комбинируем классы
classes = ['btn']
classes.append(f"btn-{variables['variant']}")
classes.append(f"btn-{variables['size']}")
if variables['disabled']:
classes.append('btn-disabled')
variables['attributes']['class'] = classes
Как в Drupal: Разные способы отображения одного поля
# entities/product.cifra
entity:
name: Product
fields:
price:
type: decimal
# Разные форматы отображения
formatters:
- default:
format: "number"
decimals: 2
prefix: "$"
- with_currency:
format: "currency"
currency: "USD"
locale: "en_US"
- short:
format: "abbreviated"
# $1.5K вместо $1,500.00
- comparison:
format: "comparison"
# Показывает старую цену зачёркнутую
show_discount_percent: true
description:
type: text
formatters:
- default:
format: "plain_text"
- teaser:
format: "summary"
length: 200
suffix: "..."
- full:
format: "html"
allowed_tags: [p, strong, em, ul, li]
# pages/user_profile.page.cifra
page:
id: user_profile
path: /users/{user_id}
title: "Профиль пользователя"
# Data (уровни абстракции)
data:
user:
source: entity
entity: User
view_model: profile
id: "{params.user_id}"
# Theme (каскад стилей)
theme:
extends: admin_light
# Переопределяем для этой страницы
overrides:
layout:
type: centered
max_width: 800px
# Components (с каскадом)
components:
header:
type: user_header
data: "{page.user}"
styles:
background: colors.surface
padding: spacing.lg
# → Разрешается через каскад
stats:
type: stats_grid
data:
- label: "Заказов"
value: "{user.orders_count}"
- label: "Потрачено"
value: "{user.total_spent}"
format: currency
orders_list:
type: data_table
data:
source: entity
entity: Order
filters:
customer_id: "{user.id}"
limit: 10
# Security
security:
auth: required
permissions:
- own: "user_id == current_user.id"
- or: "users:read"
Результат: Страница с данными через 6 уровней абстракции, стилями через каскад, OAuth авторизацией и проверкой прав!
✅ DATA ABSTRACTION (6 уровней)
DB → ORM → DTO → ViewModel → API → UI
✅ THEME SYSTEM (каскадная)
Base → Tokens → Variants → Builder → Cascade
✅ DESIGN SYSTEM
Tokens → Components → Patterns → Templates
✅ SECURITY (полная)
OAuth 2.0 → OpenID → RBAC → RLS → Field-level
✅ DRUPAL CONCEPTS
Render Arrays → Theme Hooks → Preprocess → Formatters
✅ CASCADE (универсальная)
Global → Theme → Component → Variant → State → Inline
Один .cifra файл → генерирует:
- Все уровни данных
- Все уровни стилей
- Всю безопасность
- Все хуки и форматтеры
Полная каскадность и универсальность! 🚀