This commit is contained in:
560
docs/admin-panel.md
Normal file
560
docs/admin-panel.md
Normal file
@@ -0,0 +1,560 @@
|
||||
# Администраторская панель Discours
|
||||
|
||||
## Обзор
|
||||
|
||||
Администраторская панель — это комплексная система управления платформой Discours, предоставляющая полный контроль над пользователями, публикациями, сообществами и их ролями.
|
||||
|
||||
## Архитектура системы доступа
|
||||
|
||||
### Уровни доступа
|
||||
|
||||
1. **Системные администраторы** — email в переменной `ADMIN_EMAILS` (управление системой через переменные среды)
|
||||
2. **RBAC роли в сообществах** — `reader`, `author`, `artist`, `expert`, `editor`, `admin` (управляемые через админку)
|
||||
|
||||
**ВАЖНО**:
|
||||
- Роль `admin` в RBAC — это обычная роль в сообществе, управляемая через админку
|
||||
- "Системный администратор" — синтетическая роль, которая НЕ хранится в базе данных
|
||||
- Синтетическая роль добавляется только в API ответы для пользователей из `ADMIN_EMAILS`
|
||||
- На фронте в сообществах синтетическая роль НЕ отображается
|
||||
|
||||
### Декораторы безопасности
|
||||
|
||||
```python
|
||||
@admin_auth_required # Доступ только системным админам (ADMIN_EMAILS)
|
||||
@editor_or_admin_required # Доступ редакторам и админам сообщества (RBAC роли)
|
||||
```
|
||||
|
||||
## Модули администрирования
|
||||
|
||||
### 1. Управление пользователями
|
||||
|
||||
#### Получение списка пользователей
|
||||
```graphql
|
||||
query AdminGetUsers(
|
||||
$limit: Int = 20
|
||||
$offset: Int = 0
|
||||
$search: String = ""
|
||||
) {
|
||||
adminGetUsers(limit: $limit, offset: $offset, search: $search) {
|
||||
authors {
|
||||
id
|
||||
email
|
||||
name
|
||||
slug
|
||||
roles
|
||||
created_at
|
||||
last_seen
|
||||
}
|
||||
total
|
||||
page
|
||||
perPage
|
||||
totalPages
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Особенности:**
|
||||
- Поиск по email, имени и ID
|
||||
- Пагинация с ограничением 1-100 записей
|
||||
- Роли получаются из основного сообщества (ID=1)
|
||||
- Автоматическое добавление синтетической роли "Системный администратор" для email из `ADMIN_EMAILS`
|
||||
|
||||
#### Обновление пользователя
|
||||
```graphql
|
||||
mutation AdminUpdateUser($user: AdminUserUpdateInput!) {
|
||||
adminUpdateUser(user: $user) {
|
||||
success
|
||||
error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Поддерживаемые поля:**
|
||||
- `email` — с проверкой уникальности
|
||||
- `name` — имя пользователя
|
||||
- `slug` — с проверкой уникальности
|
||||
- `roles` — массив ролей для основного сообщества
|
||||
|
||||
### 2. Система ролей и разрешений (RBAC)
|
||||
|
||||
#### Иерархия ролей
|
||||
```
|
||||
reader → author → artist → expert → editor → admin
|
||||
```
|
||||
|
||||
Каждая роль наследует права предыдущих **только при инициализации** сообщества.
|
||||
|
||||
#### Получение ролей
|
||||
```graphql
|
||||
query AdminGetRoles($community: Int) {
|
||||
adminGetRoles(community: $community) {
|
||||
id
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Без `community` — все системные роли
|
||||
- С `community` — роли конкретного сообщества + счетчик разрешений
|
||||
|
||||
#### Управление ролями в сообществах
|
||||
|
||||
**Получение ролей пользователя:**
|
||||
```graphql
|
||||
query AdminGetUserCommunityRoles(
|
||||
$author_id: Int!
|
||||
$community_id: Int!
|
||||
) {
|
||||
adminGetUserCommunityRoles(
|
||||
author_id: $author_id
|
||||
community_id: $community_id
|
||||
) {
|
||||
author_id
|
||||
community_id
|
||||
roles
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Назначение ролей:**
|
||||
```graphql
|
||||
mutation AdminSetUserCommunityRoles(
|
||||
$author_id: Int!
|
||||
$community_id: Int!
|
||||
$roles: [String!]!
|
||||
) {
|
||||
adminSetUserCommunityRoles(
|
||||
author_id: $author_id
|
||||
community_id: $community_id
|
||||
roles: $roles
|
||||
) {
|
||||
success
|
||||
error
|
||||
author_id
|
||||
community_id
|
||||
roles
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Добавление отдельной роли:**
|
||||
```graphql
|
||||
mutation AdminAddUserToRole(
|
||||
$author_id: Int!
|
||||
$role_id: String!
|
||||
$community_id: Int!
|
||||
) {
|
||||
adminAddUserToRole(
|
||||
author_id: $author_id
|
||||
role_id: $role_id
|
||||
community_id: $community_id
|
||||
) {
|
||||
success
|
||||
error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Удаление роли:**
|
||||
```graphql
|
||||
mutation AdminRemoveUserFromRole(
|
||||
$author_id: Int!
|
||||
$role_id: String!
|
||||
$community_id: Int!
|
||||
) {
|
||||
adminRemoveUserFromRole(
|
||||
author_id: $author_id
|
||||
role_id: $role_id
|
||||
community_id: $community_id
|
||||
) {
|
||||
success
|
||||
removed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Управление сообществами
|
||||
|
||||
#### Участники сообщества
|
||||
```graphql
|
||||
query AdminGetCommunityMembers(
|
||||
$community_id: Int!
|
||||
$limit: Int = 20
|
||||
$offset: Int = 0
|
||||
) {
|
||||
adminGetCommunityMembers(
|
||||
community_id: $community_id
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
) {
|
||||
members {
|
||||
id
|
||||
name
|
||||
email
|
||||
slug
|
||||
roles
|
||||
}
|
||||
total
|
||||
community_id
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Настройки ролей сообщества
|
||||
|
||||
**Получение настроек:**
|
||||
```graphql
|
||||
query AdminGetCommunityRoleSettings($community_id: Int!) {
|
||||
adminGetCommunityRoleSettings(community_id: $community_id) {
|
||||
community_id
|
||||
default_roles
|
||||
available_roles
|
||||
error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Обновление настроек:**
|
||||
```graphql
|
||||
mutation AdminUpdateCommunityRoleSettings(
|
||||
$community_id: Int!
|
||||
$default_roles: [String!]!
|
||||
$available_roles: [String!]!
|
||||
) {
|
||||
adminUpdateCommunityRoleSettings(
|
||||
community_id: $community_id
|
||||
default_roles: $default_roles
|
||||
available_roles: $available_roles
|
||||
) {
|
||||
success
|
||||
error
|
||||
community_id
|
||||
default_roles
|
||||
available_roles
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Создание пользовательской роли
|
||||
```graphql
|
||||
mutation AdminCreateCustomRole($role: CustomRoleInput!) {
|
||||
adminCreateCustomRole(role: $role) {
|
||||
success
|
||||
error
|
||||
role {
|
||||
id
|
||||
name
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Удаление пользовательской роли
|
||||
```graphql
|
||||
mutation AdminDeleteCustomRole(
|
||||
$role_id: String!
|
||||
$community_id: Int!
|
||||
) {
|
||||
adminDeleteCustomRole(
|
||||
role_id: $role_id
|
||||
community_id: $community_id
|
||||
) {
|
||||
success
|
||||
error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Управление публикациями
|
||||
|
||||
#### Получение списка публикаций
|
||||
```graphql
|
||||
query AdminGetShouts(
|
||||
$limit: Int = 20
|
||||
$offset: Int = 0
|
||||
$search: String = ""
|
||||
$status: String = "all"
|
||||
$community: Int
|
||||
) {
|
||||
adminGetShouts(
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
search: $search
|
||||
status: $status
|
||||
community: $community
|
||||
) {
|
||||
shouts {
|
||||
id
|
||||
title
|
||||
slug
|
||||
body
|
||||
lead
|
||||
subtitle
|
||||
# ... остальные поля
|
||||
created_by {
|
||||
id
|
||||
email
|
||||
name
|
||||
slug
|
||||
}
|
||||
community {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
authors {
|
||||
id
|
||||
email
|
||||
name
|
||||
slug
|
||||
}
|
||||
topics {
|
||||
id
|
||||
title
|
||||
slug
|
||||
}
|
||||
}
|
||||
total
|
||||
page
|
||||
perPage
|
||||
totalPages
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Статусы публикаций:**
|
||||
- `all` — все публикации (включая удаленные)
|
||||
- `published` — опубликованные
|
||||
- `draft` — черновики
|
||||
- `deleted` — удаленные
|
||||
|
||||
#### Операции с публикациями
|
||||
|
||||
**Обновление:**
|
||||
```graphql
|
||||
mutation AdminUpdateShout($shout: AdminShoutUpdateInput!) {
|
||||
adminUpdateShout(shout: $shout) {
|
||||
success
|
||||
error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Удаление (мягкое):**
|
||||
```graphql
|
||||
mutation AdminDeleteShout($shout_id: Int!) {
|
||||
adminDeleteShout(shout_id: $shout_id) {
|
||||
success
|
||||
error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Восстановление:**
|
||||
```graphql
|
||||
mutation AdminRestoreShout($shout_id: Int!) {
|
||||
adminRestoreShout(shout_id: $shout_id) {
|
||||
success
|
||||
error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Управление приглашениями
|
||||
|
||||
#### Получение списка приглашений
|
||||
```graphql
|
||||
query AdminGetInvites(
|
||||
$limit: Int = 20
|
||||
$offset: Int = 0
|
||||
$search: String = ""
|
||||
$status: String = "all"
|
||||
) {
|
||||
adminGetInvites(
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
search: $search
|
||||
status: $status
|
||||
) {
|
||||
invites {
|
||||
inviter_id
|
||||
author_id
|
||||
shout_id
|
||||
status
|
||||
inviter {
|
||||
id
|
||||
email
|
||||
name
|
||||
slug
|
||||
}
|
||||
author {
|
||||
id
|
||||
email
|
||||
name
|
||||
slug
|
||||
}
|
||||
shout {
|
||||
id
|
||||
title
|
||||
slug
|
||||
created_by {
|
||||
id
|
||||
email
|
||||
name
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
total
|
||||
page
|
||||
perPage
|
||||
totalPages
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Статусы приглашений:**
|
||||
- `PENDING` — ожидает ответа
|
||||
- `ACCEPTED` — принято
|
||||
- `REJECTED` — отклонено
|
||||
|
||||
#### Операции с приглашениями
|
||||
|
||||
**Обновление статуса:**
|
||||
```graphql
|
||||
mutation AdminUpdateInvite($invite: AdminInviteUpdateInput!) {
|
||||
adminUpdateInvite(invite: $invite) {
|
||||
success
|
||||
error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Удаление:**
|
||||
```graphql
|
||||
mutation AdminDeleteInvite(
|
||||
$inviter_id: Int!
|
||||
$author_id: Int!
|
||||
$shout_id: Int!
|
||||
) {
|
||||
adminDeleteInvite(
|
||||
inviter_id: $inviter_id
|
||||
author_id: $author_id
|
||||
shout_id: $shout_id
|
||||
) {
|
||||
success
|
||||
error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Пакетное удаление:**
|
||||
```graphql
|
||||
mutation AdminDeleteInvitesBatch($invites: [AdminInviteIdInput!]!) {
|
||||
adminDeleteInvitesBatch(invites: $invites) {
|
||||
success
|
||||
error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Переменные окружения
|
||||
|
||||
Системные администраторы могут управлять переменными окружения:
|
||||
|
||||
```graphql
|
||||
query GetEnvVariables {
|
||||
getEnvVariables {
|
||||
name
|
||||
description
|
||||
variables {
|
||||
key
|
||||
value
|
||||
description
|
||||
type
|
||||
isSecret
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```graphql
|
||||
mutation UpdateEnvVariable($key: String!, $value: String!) {
|
||||
updateEnvVariable(key: $key, value: $value) {
|
||||
success
|
||||
error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Особенности реализации
|
||||
|
||||
### Принцип DRY
|
||||
- Переиспользование логики из `reader.py`, `editor.py`
|
||||
- Общие утилиты в `_get_user_roles()`
|
||||
- Централизованная обработка ошибок
|
||||
|
||||
### Новая RBAC система
|
||||
- Роли хранятся в CSV формате в `CommunityAuthor.roles`
|
||||
- Методы модели: `add_role()`, `remove_role()`, `set_roles()`, `has_role()`
|
||||
- Права наследуются **только при инициализации**
|
||||
- Redis кэширование развернутых прав
|
||||
|
||||
### Синтетические роли
|
||||
- **"Системный администратор"** — добавляется автоматически для пользователей из `ADMIN_EMAILS`
|
||||
- НЕ хранится в базе данных, только в API ответах
|
||||
- НЕ отображается на фронте в интерфейсах управления сообществами
|
||||
- Используется только для индикации системных прав доступа
|
||||
|
||||
### Безопасность
|
||||
- Валидация всех входных данных
|
||||
- Проверка существования сущностей
|
||||
- Контроль доступа через декораторы
|
||||
- Логирование всех административных действий
|
||||
|
||||
### Производительность
|
||||
- Пагинация для всех списков
|
||||
- Индексы по ключевым полям
|
||||
- Ограничения на размер выборки (max 100)
|
||||
- Оптимизированные SQL запросы с `joinedload`
|
||||
|
||||
## Миграция данных
|
||||
|
||||
При переходе на новую RBAC систему используется функция:
|
||||
|
||||
```python
|
||||
from orm.community import migrate_old_roles_to_community_author
|
||||
migrate_old_roles_to_community_author()
|
||||
```
|
||||
|
||||
Функция автоматически переносит роли из старых таблиц в новый формат CSV.
|
||||
|
||||
## Мониторинг и логирование
|
||||
|
||||
Все административные действия логируются с уровнем INFO:
|
||||
- Изменение ролей пользователей
|
||||
- Обновление настроек сообществ
|
||||
- Операции с публикациями
|
||||
- Управление приглашениями
|
||||
|
||||
Ошибки логируются с уровнем ERROR и полным стектрейсом.
|
||||
|
||||
## Лучшие практики
|
||||
|
||||
1. **Всегда проверяйте роли перед назначением**
|
||||
2. **Используйте транзакции для групповых операций**
|
||||
3. **Логируйте критические изменения**
|
||||
4. **Валидируйте права доступа на каждом этапе**
|
||||
5. **Применяйте принцип минимальных привилегий**
|
||||
|
||||
## Расширение функциональности
|
||||
|
||||
Для добавления новых административных функций:
|
||||
|
||||
1. Создайте резолвер с соответствующим декоратором
|
||||
2. Добавьте GraphQL схему в `schema/admin.graphql`
|
||||
3. Реализуйте логику с переиспользованием существующих компонентов
|
||||
4. Добавьте тесты и документацию
|
||||
5. Обновите права доступа при необходимости
|
10
docs/auth.md
10
docs/auth.md
@@ -16,7 +16,7 @@
|
||||
- Блокировку аккаунта при множественных неудачных попытках входа
|
||||
- Верификацию email/телефона
|
||||
|
||||
#### Role и Permission (orm.py)
|
||||
#### Role и Permission (resolvers/rbac.py)
|
||||
- Реализация RBAC (Role-Based Access Control)
|
||||
- Роли содержат наборы разрешений
|
||||
- Разрешения определяются как пары resource:operation
|
||||
@@ -307,7 +307,7 @@ async def create_article_example(request: Request): # Используем Reque
|
||||
user: Author = request.user # request.user добавляется декоратором @login_required
|
||||
|
||||
# Проверяем право на создание статей (метод из модели auth.auth.orm)
|
||||
if not user.has_permission('articles', 'create'):
|
||||
if not await user.has_permission('shout:create'):
|
||||
return JSONResponse({'error': 'Недостаточно прав для создания статьи'}, status_code=403)
|
||||
|
||||
try:
|
||||
@@ -361,7 +361,7 @@ async def update_article(_: None,info, article_id: int, data: dict):
|
||||
raise GraphQLError('Статья не найдена')
|
||||
|
||||
# Проверяем права на редактирование
|
||||
if not user.has_permission('articles', 'edit'):
|
||||
if not await user.has_permission('articles', 'edit'):
|
||||
raise GraphQLError('Недостаточно прав')
|
||||
|
||||
# Обновляем поля
|
||||
@@ -677,8 +677,8 @@ def test_user_permissions():
|
||||
user.roles.append(role)
|
||||
|
||||
# Проверяем разрешения
|
||||
assert user.has_permission('articles', 'edit')
|
||||
assert not user.has_permission('articles', 'delete')
|
||||
assert await user.has_permission('articles', 'edit')
|
||||
assert not await user.has_permission('articles', 'delete')
|
||||
```
|
||||
|
||||
## Безопасность
|
||||
|
@@ -159,3 +159,15 @@
|
||||
- Обработка в `create_reaction` для новых реакций
|
||||
- Обработка в `delete_reaction` для удаленных реакций
|
||||
- Учет только реакций на саму публикацию (не на комментарии)
|
||||
|
||||
## RBAC
|
||||
|
||||
- **Наследование разрешений между ролями** происходит только при инициализации прав для сообщества. В Redis хранятся уже развернутые (полные) списки разрешений для каждой роли. Проверка прав — это быстрый lookup без on-the-fly наследования.
|
||||
|
||||
## Core features
|
||||
|
||||
- RBAC с иерархией ролей, наследование только при инициализации, быстрый доступ к правам через Redis
|
||||
|
||||
## Changelog
|
||||
|
||||
- v0.6.11: RBAC — наследование только при инициализации, ускорение, упрощение кода, исправлены тесты
|
||||
|
369
docs/rbac-system.md
Normal file
369
docs/rbac-system.md
Normal file
@@ -0,0 +1,369 @@
|
||||
# Система RBAC (Role-Based Access Control)
|
||||
|
||||
## Обзор
|
||||
|
||||
Система управления доступом на основе ролей для платформы Discours. Роли хранятся в CSV формате в таблице `CommunityAuthor` и могут быть назначены пользователям в рамках конкретного сообщества.
|
||||
|
||||
> **v0.6.11: Важно!** Наследование разрешений между ролями происходит **только при инициализации** прав для сообщества. В Redis хранятся уже развернутые (полные) списки разрешений для каждой роли. При запросе прав никакого on-the-fly наследования не происходит — только lookup по роли.
|
||||
|
||||
## Архитектура
|
||||
|
||||
### Основные принципы
|
||||
- **CSV хранение**: Роли хранятся как CSV строка в поле `roles` таблицы `CommunityAuthor`
|
||||
- **Простота**: Один пользователь может иметь несколько ролей в одном сообществе
|
||||
- **Привязка к сообществу**: Роли существуют в контексте конкретного сообщества
|
||||
- **Иерархия ролей**: `reader` → `author` → `artist` → `expert` → `editor` → `admin`
|
||||
- **Наследование прав**: Каждая роль наследует все права предыдущих ролей **только при инициализации**
|
||||
|
||||
### Схема базы данных
|
||||
|
||||
#### Таблица `community_author`
|
||||
```sql
|
||||
CREATE TABLE community_author (
|
||||
id INTEGER PRIMARY KEY,
|
||||
community_id INTEGER REFERENCES community(id) NOT NULL,
|
||||
author_id INTEGER REFERENCES author(id) NOT NULL,
|
||||
roles TEXT, -- CSV строка ролей ("reader,author,expert")
|
||||
joined_at INTEGER NOT NULL, -- Unix timestamp присоединения
|
||||
|
||||
CONSTRAINT uq_community_author UNIQUE (community_id, author_id)
|
||||
);
|
||||
```
|
||||
|
||||
#### Индексы
|
||||
```sql
|
||||
CREATE INDEX idx_community_author_community ON community_author(community_id);
|
||||
CREATE INDEX idx_community_author_author ON community_author(author_id);
|
||||
```
|
||||
|
||||
## Работа с ролями
|
||||
|
||||
### Модель CommunityAuthor
|
||||
|
||||
#### Основные методы
|
||||
```python
|
||||
from orm.community import CommunityAuthor
|
||||
|
||||
# Получение списка ролей
|
||||
ca = session.query(CommunityAuthor).first()
|
||||
roles = ca.role_list # ['reader', 'author', 'expert']
|
||||
|
||||
# Установка ролей
|
||||
ca.role_list = ['reader', 'author']
|
||||
|
||||
# Проверка роли
|
||||
has_author = ca.has_role('author') # True
|
||||
|
||||
# Добавление роли
|
||||
ca.add_role('expert')
|
||||
|
||||
# Удаление роли
|
||||
ca.remove_role('author')
|
||||
|
||||
# Установка полного списка ролей
|
||||
ca.set_roles(['reader', 'editor'])
|
||||
|
||||
# Получение всех разрешений
|
||||
permissions = await ca.get_permissions() # ['shout:read', 'shout:create', ...]
|
||||
|
||||
# Проверка разрешения
|
||||
can_create = await ca.has_permission('shout:create') # True
|
||||
```
|
||||
|
||||
### Вспомогательные функции
|
||||
|
||||
#### Основные функции из `orm/community.py`
|
||||
```python
|
||||
from orm.community import (
|
||||
get_user_roles_in_community,
|
||||
check_user_permission_in_community,
|
||||
assign_role_to_user,
|
||||
remove_role_from_user,
|
||||
get_all_community_members_with_roles,
|
||||
bulk_assign_roles
|
||||
)
|
||||
|
||||
# Получение ролей пользователя
|
||||
roles = get_user_roles_in_community(author_id=123, community_id=1)
|
||||
# Возвращает: ['reader', 'author']
|
||||
|
||||
# Проверка разрешения
|
||||
has_perm = await check_user_permission_in_community(
|
||||
author_id=123,
|
||||
permission='shout:create',
|
||||
community_id=1
|
||||
)
|
||||
|
||||
# Назначение роли
|
||||
success = assign_role_to_user(
|
||||
author_id=123,
|
||||
role='expert',
|
||||
community_id=1
|
||||
)
|
||||
|
||||
# Удаление роли
|
||||
success = remove_role_from_user(
|
||||
author_id=123,
|
||||
role='author',
|
||||
community_id=1
|
||||
)
|
||||
|
||||
# Получение всех участников с ролями
|
||||
members = get_all_community_members_with_roles(community_id=1)
|
||||
# Возвращает: [{'author_id': 123, 'roles': ['reader', 'author'], ...}, ...]
|
||||
|
||||
# Массовое назначение ролей
|
||||
bulk_assign_roles([
|
||||
{'author_id': 123, 'roles': ['reader', 'author']},
|
||||
{'author_id': 456, 'roles': ['expert', 'editor']}
|
||||
], community_id=1)
|
||||
```
|
||||
|
||||
## Система разрешений
|
||||
|
||||
### Иерархия ролей
|
||||
```
|
||||
reader → author → artist → expert → editor → admin
|
||||
```
|
||||
|
||||
Каждая роль наследует все права предыдущих ролей в дефолтной иерархии **только при создании сообщества**.
|
||||
|
||||
### Стандартные роли и их права
|
||||
|
||||
| Роль | Базовые права | Дополнительные права |
|
||||
|------|---------------|---------------------|
|
||||
| `reader` | `*:read`, базовые реакции | `chat:*`, `message:*` |
|
||||
| `author` | Наследует `reader` + `*:create`, `*:update_own`, `*:delete_own` | `draft:*` |
|
||||
| `artist` | Наследует `author` | `reaction:CREDIT:accept`, `reaction:CREDIT:decline` |
|
||||
| `expert` | Наследует `author` | `reaction:PROOF:*`, `reaction:DISPROOF:*`, `reaction:AGREE:*`, `reaction:DISAGREE:*` |
|
||||
| `editor` | `*:read`, `*:create`, `*:update_any`, `*:delete_any` | `community:read`, `community:update_own` |
|
||||
| `admin` | Все права (`*`) | Полный доступ ко всем функциям |
|
||||
|
||||
### Формат разрешений
|
||||
- Базовые: `<entity>:<action>` (например: `shout:create`)
|
||||
- Реакции: `reaction:<type>:<action>` (например: `reaction:LIKE:create`)
|
||||
- Wildcard: `<entity>:*` или `*` (только для admin)
|
||||
|
||||
## GraphQL API
|
||||
|
||||
### Запросы
|
||||
|
||||
#### Получение участников сообщества с ролями
|
||||
```graphql
|
||||
query AdminGetCommunityMembers(
|
||||
$community_id: Int!
|
||||
$page: Int = 1
|
||||
$limit: Int = 50
|
||||
) {
|
||||
adminGetCommunityMembers(
|
||||
community_id: $community_id
|
||||
page: $page
|
||||
limit: $limit
|
||||
) {
|
||||
success
|
||||
error
|
||||
members {
|
||||
id
|
||||
name
|
||||
slug
|
||||
email
|
||||
roles
|
||||
is_follower
|
||||
created_at
|
||||
}
|
||||
total
|
||||
page
|
||||
limit
|
||||
has_next
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Мутации
|
||||
|
||||
#### Назначение ролей пользователю
|
||||
```graphql
|
||||
mutation AdminSetUserCommunityRoles(
|
||||
$author_id: Int!
|
||||
$community_id: Int!
|
||||
$roles: [String!]!
|
||||
) {
|
||||
adminSetUserCommunityRoles(
|
||||
author_id: $author_id
|
||||
community_id: $community_id
|
||||
roles: $roles
|
||||
) {
|
||||
success
|
||||
error
|
||||
author_id
|
||||
community_id
|
||||
roles
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Обновление настроек ролей сообщества
|
||||
```graphql
|
||||
mutation AdminUpdateCommunityRoleSettings(
|
||||
$community_id: Int!
|
||||
$default_roles: [String!]!
|
||||
$available_roles: [String!]!
|
||||
) {
|
||||
adminUpdateCommunityRoleSettings(
|
||||
community_id: $community_id
|
||||
default_roles: $default_roles
|
||||
available_roles: $available_roles
|
||||
) {
|
||||
success
|
||||
error
|
||||
community_id
|
||||
default_roles
|
||||
available_roles
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Использование декораторов RBAC
|
||||
|
||||
### Импорт декораторов
|
||||
```python
|
||||
from resolvers.rbac import (
|
||||
require_permission, require_role, admin_only,
|
||||
authenticated_only, require_any_permission,
|
||||
require_all_permissions, RBACError
|
||||
)
|
||||
```
|
||||
|
||||
### Примеры использования
|
||||
|
||||
#### Проверка конкретного разрешения
|
||||
```python
|
||||
@mutation.field("createShout")
|
||||
@require_permission("shout:create")
|
||||
async def create_shout(self, info: GraphQLResolveInfo, **kwargs):
|
||||
# Только пользователи с правом создания статей
|
||||
return await self._create_shout_logic(**kwargs)
|
||||
```
|
||||
|
||||
#### Проверка любого из разрешений (OR логика)
|
||||
```python
|
||||
@mutation.field("updateShout")
|
||||
@require_any_permission(["shout:update_own", "shout:update_any"])
|
||||
async def update_shout(self, info: GraphQLResolveInfo, shout_id: int, **kwargs):
|
||||
# Может редактировать свои статьи ИЛИ любые статьи
|
||||
return await self._update_shout_logic(shout_id, **kwargs)
|
||||
```
|
||||
|
||||
#### Проверка конкретной роли
|
||||
```python
|
||||
@mutation.field("verifyEvidence")
|
||||
@require_role("expert")
|
||||
async def verify_evidence(self, info: GraphQLResolveInfo, **kwargs):
|
||||
# Только эксперты могут верифицировать доказательства
|
||||
return await self._verify_evidence_logic(**kwargs)
|
||||
```
|
||||
|
||||
#### Только для администраторов
|
||||
```python
|
||||
@mutation.field("deleteAnyContent")
|
||||
@admin_only
|
||||
async def delete_any_content(self, info: GraphQLResolveInfo, content_id: int):
|
||||
# Только администраторы
|
||||
return await self._delete_content_logic(content_id)
|
||||
```
|
||||
|
||||
### Обработка ошибок
|
||||
```python
|
||||
from resolvers.rbac import RBACError
|
||||
|
||||
try:
|
||||
result = await some_rbac_protected_function()
|
||||
except RBACError as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
```
|
||||
|
||||
## Настройка сообщества
|
||||
|
||||
### Управление ролями в сообществе
|
||||
```python
|
||||
from orm.community import Community
|
||||
|
||||
community = session.query(Community).filter(Community.id == 1).first()
|
||||
|
||||
# Установка доступных ролей
|
||||
community.set_available_roles(['reader', 'author', 'expert', 'admin'])
|
||||
|
||||
# Установка дефолтных ролей для новых участников
|
||||
community.set_default_roles(['reader'])
|
||||
|
||||
# Получение настроек
|
||||
available = community.get_available_roles() # ['reader', 'author', 'expert', 'admin']
|
||||
default = community.get_default_roles() # ['reader']
|
||||
```
|
||||
|
||||
### Автоматическое назначение дефолтных ролей
|
||||
При создании связи пользователя с сообществом автоматически назначаются роли из `default_roles`.
|
||||
|
||||
## Интеграция с GraphQL контекстом
|
||||
|
||||
### Middleware для установки ролей
|
||||
```python
|
||||
async def rbac_middleware(request, call_next):
|
||||
# Получаем автора из контекста
|
||||
author = getattr(request.state, 'author', None)
|
||||
if author:
|
||||
# Устанавливаем роли в контекст для текущего сообщества
|
||||
community_id = get_current_community_id(request)
|
||||
if community_id:
|
||||
user_roles = get_user_roles_in_community(author.id, community_id)
|
||||
request.state.user_roles = user_roles
|
||||
|
||||
response = await call_next(request)
|
||||
return response
|
||||
```
|
||||
|
||||
### Получение ролей в resolver'ах
|
||||
```python
|
||||
def get_user_roles_from_context(info):
|
||||
"""Получение ролей пользователя из GraphQL контекста"""
|
||||
# Из middleware
|
||||
user_roles = getattr(info.context, "user_roles", [])
|
||||
if user_roles:
|
||||
return user_roles
|
||||
|
||||
# Из author'а напрямую
|
||||
author = getattr(info.context, "author", None)
|
||||
if author and hasattr(author, "roles"):
|
||||
return author.roles.split(",") if author.roles else []
|
||||
|
||||
return []
|
||||
```
|
||||
|
||||
## Миграция и обновления
|
||||
|
||||
### Миграция с предыдущей системы ролей
|
||||
Если в проекте была отдельная таблица ролей, необходимо:
|
||||
|
||||
1. Создать миграцию для добавления поля `roles` в `CommunityAuthor`
|
||||
2. Перенести данные из старых таблиц в CSV формат
|
||||
3. Удалить старые таблицы ролей
|
||||
|
||||
```bash
|
||||
alembic revision --autogenerate -m "Add CSV roles to CommunityAuthor"
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
### Обновление CHANGELOG.md
|
||||
После внесения изменений в RBAC систему обновляется `CHANGELOG.md` с новой версией.
|
||||
|
||||
## Производительность
|
||||
|
||||
### Оптимизация
|
||||
- CSV роли хранятся в одном поле, что снижает количество JOIN'ов
|
||||
- Индексы на `community_id` и `author_id` ускоряют запросы
|
||||
- Кеширование разрешений на уровне приложения
|
||||
|
||||
### Рекомендации
|
||||
- Избегать частых изменений ролей
|
||||
- Кешировать результаты `get_role_permissions_for_community()`
|
||||
- Использовать bulk операции для массового назначения ролей
|
378
docs/react-to-solidjs.md
Normal file
378
docs/react-to-solidjs.md
Normal file
@@ -0,0 +1,378 @@
|
||||
# Миграция с React 18 на SolidStart: Comprehensive Guide
|
||||
|
||||
## 1. Введение
|
||||
|
||||
### 1.1 Что такое SolidStart?
|
||||
|
||||
SolidStart - это метафреймворк для SolidJS, который предоставляет полнофункциональное решение для создания веб-приложений. Ключевые особенности:
|
||||
|
||||
- Полностью изоморфное приложение (работает на клиенте и сервере)
|
||||
- Встроенная поддержка SSR, SSG и CSR
|
||||
- Интеграция с Vite и Nitro
|
||||
- Гибкая маршрутизация
|
||||
- Встроенные серверные функции и действия
|
||||
|
||||
### 1.2 Основные различия между React и SolidStart
|
||||
|
||||
| Характеристика | React 18 | SolidStart |
|
||||
|---------------|----------|------------|
|
||||
| Рендеринг | Virtual DOM | Компиляция и прямое обновление DOM |
|
||||
| Серверный рендеринг | Сложная настройка | Встроенная поддержка |
|
||||
| Размер бандла | ~40 кБ | ~7.7 кБ |
|
||||
| Реактивность | Хуки с зависимостями | Сигналы без явных зависимостей |
|
||||
| Маршрутизация | react-router | @solidjs/router |
|
||||
|
||||
## 2. Подготовка проекта
|
||||
|
||||
### 2.1 Установка зависимостей
|
||||
|
||||
```bash
|
||||
# Удаление React зависимостей
|
||||
npm uninstall react react-dom react-router-dom
|
||||
|
||||
# Установка SolidStart и связанных библиотек
|
||||
npm install @solidjs/start solid-js @solidjs/router
|
||||
```
|
||||
|
||||
### 2.2 Обновление конфигурации
|
||||
|
||||
#### Vite Configuration (`vite.config.ts`)
|
||||
```typescript
|
||||
import { defineConfig } from 'vite';
|
||||
import solid from 'solid-start/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solid()],
|
||||
// Дополнительные настройки
|
||||
});
|
||||
```
|
||||
|
||||
#### TypeScript Configuration (`tsconfig.json`)
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"types": ["solid-start/env"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### SolidStart Configuration (`app.config.ts`)
|
||||
```typescript
|
||||
import { defineConfig } from "@solidjs/start/config";
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
// Настройки сервера, например:
|
||||
preset: "netlify" // или другой провайдер
|
||||
},
|
||||
// Дополнительные настройки
|
||||
});
|
||||
```
|
||||
|
||||
## 3. Миграция компонентов и логики
|
||||
|
||||
### 3.1 Состояние и реактивность
|
||||
|
||||
#### React:
|
||||
```typescript
|
||||
const [count, setCount] = useState(0);
|
||||
```
|
||||
|
||||
#### SolidJS:
|
||||
```typescript
|
||||
const [count, setCount] = createSignal(0);
|
||||
// Использование: count(), setCount(newValue)
|
||||
```
|
||||
|
||||
### 3.2 Серверные функции и загрузка данных
|
||||
|
||||
В SolidStart есть несколько способов работы с данными:
|
||||
|
||||
#### Серверная функция
|
||||
```typescript
|
||||
// server/api.ts
|
||||
export function getUser(id: string) {
|
||||
return db.users.findUnique({ where: { id } });
|
||||
}
|
||||
|
||||
// Component
|
||||
export default function UserProfile() {
|
||||
const user = createAsync(() => getUser(params.id));
|
||||
|
||||
return <div>{user()?.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
#### Действия (Actions)
|
||||
```typescript
|
||||
export function updateProfile(formData: FormData) {
|
||||
'use server';
|
||||
const name = formData.get('name');
|
||||
// Логика обновления профиля
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Маршрутизация
|
||||
|
||||
```typescript
|
||||
// src/routes/index.tsx
|
||||
import { A } from "@solidjs/router";
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div>
|
||||
<A href="/about">О нас</A>
|
||||
<A href="/profile">Профиль</A>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// src/routes/profile.tsx
|
||||
export default function ProfilePage() {
|
||||
return <div>Профиль пользователя</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Оптимизация и производительность
|
||||
|
||||
### 4.1 Мемоизация
|
||||
|
||||
```typescript
|
||||
// Кэширование сложных вычислений
|
||||
const sortedUsers = createMemo(() =>
|
||||
users().sort((a, b) => a.name.localeCompare(b.name))
|
||||
);
|
||||
|
||||
// Ленивая загрузка
|
||||
const UserList = lazy(() => import('./UserList'));
|
||||
```
|
||||
|
||||
### 4.2 Серверный рендеринг и предзагрузка
|
||||
|
||||
```typescript
|
||||
// Предзагрузка данных
|
||||
export function routeData() {
|
||||
return {
|
||||
user: createAsync(() => fetchUser())
|
||||
};
|
||||
}
|
||||
|
||||
export default function UserPage() {
|
||||
const user = useRouteData<typeof routeData>();
|
||||
return <div>{user().name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Особенности миграции
|
||||
|
||||
### 5.1 Ключевые изменения
|
||||
- Замена `useState` на `createSignal`
|
||||
- Использование `createAsync` вместо `useEffect` для загрузки данных
|
||||
- Серверные функции с `'use server'`
|
||||
- Маршрутизация через `@solidjs/router`
|
||||
|
||||
### 5.2 Потенциальные проблемы
|
||||
- Переписать все React-специфичные хуки
|
||||
- Адаптировать библиотеки компонентов
|
||||
- Обновить тесты и CI/CD
|
||||
|
||||
## 6. Деплой
|
||||
|
||||
SolidStart поддерживает множество платформ:
|
||||
- Netlify
|
||||
- Vercel
|
||||
- Cloudflare
|
||||
- AWS
|
||||
- Deno
|
||||
- и другие
|
||||
|
||||
```typescript
|
||||
// app.config.ts
|
||||
export default defineConfig({
|
||||
server: {
|
||||
preset: "netlify" // Выберите вашу платформу
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 7. Инструменты и экосистема
|
||||
|
||||
### Рекомендованные библиотеки
|
||||
- Роутинг: `@solidjs/router`
|
||||
- Состояние: Встроенные примитивы SolidJS
|
||||
- Запросы: `@tanstack/solid-query`
|
||||
- Девтулзы: `solid-devtools`
|
||||
|
||||
## 8. Миграция конкретных компонентов
|
||||
|
||||
### 8.1 Страница регистрации (RegisterPage)
|
||||
|
||||
#### React-версия
|
||||
```typescript
|
||||
import React from 'react'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import { RegisterForm } from '../components/auth/RegisterForm'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
|
||||
export const RegisterPage: React.FC = () => {
|
||||
const { isAuthenticated } = useAuthStore()
|
||||
|
||||
if (isAuthenticated) {
|
||||
return <Navigate to="/" replace />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen ...">
|
||||
<RegisterForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### SolidJS-версия
|
||||
```typescript
|
||||
import { Navigate } from '@solidjs/router'
|
||||
import { Show } from 'solid-js'
|
||||
import { RegisterForm } from '../components/auth/RegisterForm'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
|
||||
export default function RegisterPage() {
|
||||
const { isAuthenticated } = useAuthStore()
|
||||
|
||||
return (
|
||||
<Show when={!isAuthenticated()} fallback={<Navigate href="/" />}>
|
||||
<div class="min-h-screen ...">
|
||||
<RegisterForm />
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### Ключевые изменения
|
||||
- Удаление импорта React
|
||||
- Использование `@solidjs/router` вместо `react-router-dom`
|
||||
- Замена `className` на `class`
|
||||
- Использование `Show` для условного рендеринга
|
||||
- Вызов `isAuthenticated()` как функции
|
||||
- Использование `href` вместо `to`
|
||||
- Экспорт по умолчанию вместо именованного экспорта
|
||||
|
||||
### Рекомендации
|
||||
- Всегда используйте `Show` для условного рендеринга
|
||||
- Помните, что сигналы в SolidJS - это функции
|
||||
- Следите за совместимостью импортов и маршрутизации
|
||||
|
||||
## 9. UI Component Migration
|
||||
|
||||
### 9.1 Key Differences in Component Structure
|
||||
|
||||
When migrating UI components from React to SolidJS, several key changes are necessary:
|
||||
|
||||
1. **Props Handling**
|
||||
- Replace `React.FC<Props>` with function component syntax
|
||||
- Use object destructuring for props instead of individual parameters
|
||||
- Replace `className` with `class`
|
||||
- Use `props.children` instead of `children` prop
|
||||
|
||||
2. **Type Annotations**
|
||||
- Use TypeScript interfaces for props
|
||||
- Explicitly type `children` as `any` or a more specific type
|
||||
- Remove React-specific type imports
|
||||
|
||||
3. **Event Handling**
|
||||
- Use SolidJS event types (e.g., `InputEvent`)
|
||||
- Modify event handler signatures to match SolidJS conventions
|
||||
|
||||
### 9.2 Component Migration Example
|
||||
|
||||
#### React Component
|
||||
```typescript
|
||||
import React from 'react'
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary'
|
||||
fullWidth?: boolean
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
variant = 'primary',
|
||||
fullWidth = false,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const classes = clsx(
|
||||
'button',
|
||||
variant === 'primary' && 'bg-blue-500',
|
||||
fullWidth && 'w-full',
|
||||
className
|
||||
)
|
||||
|
||||
return (
|
||||
<button className={classes} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### SolidJS Component
|
||||
```typescript
|
||||
import { clsx } from 'clsx'
|
||||
|
||||
interface ButtonProps {
|
||||
variant?: 'primary' | 'secondary'
|
||||
fullWidth?: boolean
|
||||
class?: string
|
||||
children: any
|
||||
disabled?: boolean
|
||||
type?: 'button' | 'submit'
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export const Button = (props: ButtonProps) => {
|
||||
const classes = clsx(
|
||||
'button',
|
||||
props.variant === 'primary' && 'bg-blue-500',
|
||||
props.fullWidth && 'w-full',
|
||||
props.class
|
||||
)
|
||||
|
||||
return (
|
||||
<button
|
||||
class={classes}
|
||||
disabled={props.disabled}
|
||||
type={props.type || 'button'}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 Key Migration Strategies
|
||||
|
||||
- Replace `React.FC` with standard function components
|
||||
- Use `props` object instead of individual parameters
|
||||
- Replace `className` with `class`
|
||||
- Modify event handling to match SolidJS patterns
|
||||
- Remove React-specific lifecycle methods
|
||||
- Use SolidJS primitives like `createEffect` for side effects
|
||||
|
||||
## Заключение
|
||||
|
||||
Миграция на SolidStart требует внимательного подхода, но предоставляет значительные преимущества в производительности, простоте разработки и серверных возможностях.
|
||||
|
||||
### Рекомендации
|
||||
- Мигрируйте постепенно
|
||||
- Пишите тесты на каждом этапе
|
||||
- Используйте инструменты совместимости
|
||||
|
||||
---
|
||||
|
||||
Этот гайд поможет вам систематически и безопасно мигрировать ваш проект на SolidStart, сохраняя существующую функциональность и улучшая производительность.
|
Reference in New Issue
Block a user