2026-04-09 16:47:51 +05:00
2026-04-02 12:00:11 +05:00
2026-02-27 15:37:15 +05:00
2026-04-09 16:43:59 +05:00
2026-04-09 16:47:51 +05:00
2026-03-31 13:25:14 +05:00
2026-02-03 17:15:58 +05:00
2026-04-02 12:00:11 +05:00
2026-03-31 13:25:14 +05:00
2026-04-02 14:14:08 +05:00
2026-03-01 21:40:09 +05:00
2026-03-31 13:25:14 +05:00
2026-04-09 16:43:59 +05:00
2026-04-02 14:14:08 +05:00
2026-04-02 12:00:11 +05:00
2026-04-07 15:37:04 +05:00
2026-04-02 12:00:11 +05:00
2026-04-06 15:40:06 +05:00
2026-04-09 16:43:59 +05:00
2026-03-23 21:28:10 +05:00
2026-03-23 21:28:10 +05:00
2026-04-09 16:43:59 +05:00

api-copp

Платформа ЦОПП Челябинской области для проведения профориентационных тестирований. Включает REST API на FastAPI и пользовательский интерфейс на Flet. Поддерживает тесты Голланда, Климова и Гломштока с паутинными диаграммами результатов.


Содержание


Архитектура

Пользователь (браузер)
       │
       │ :80
 [ Внешний прокси / nginx ]   ← необязателен для локальной сети
       │
       │ :80
  [ web — Flet ASGI ]         ← единственный публичный сервис
       │
       │ http://api:8000  (только внутри Docker-сети)
  [ api — FastAPI ]
       │              │
 [ PostgreSQL :5432 ] [ Redis :6379 ]

Важно: сервис api не публикует порты наружу — он доступен только контейнеру web внутри Docker-сети. Пользователь никогда не обращается к API напрямую.

Сетевая топология Docker: PostgreSQL и Redis запускаются отдельным compose-файлом docker-compose_db.yml и подключаются к той же Docker network.


Быстрый старт

1. Инфраструктура (PostgreSQL + Redis)

docker compose -f docker-compose_db.yml up -d

2. Приложение (API + Web)

docker compose up -d --build

После запуска:

Адрес Что
http://localhost Веб-интерфейс (Flet)
http://localhost:8000/docs Swagger UI — только с хоста сервера, порт не опубликован
http://localhost:8000/redoc ReDoc

В продакшне API недоступен снаружи. Для работы с /docs используйте SSH-туннель:

ssh -L 8000:localhost:8000 user@your-server

Доступ к документации — логин/пароль из переменных DOCS_USERNAME / DOCS_PASSWORD.

3. Инициализация БД

После первого запуска выполните (с хоста сервера или через туннель):

# Создать таблицы
curl -X POST http://localhost:8000/db/create-tables \
  -H "X-Admin-Key: <ADMIN_KEY>"

# Применить миграции (добавляет колонки, если БД уже была создана ранее)
curl -X POST http://localhost:8000/db/migrate \
  -H "X-Admin-Key: <ADMIN_KEY>"

4. Загрузка теста Голланда

# Создать тест (возвращает poll_id)
curl -X POST http://localhost:8000/polls/holland \
  -H "X-Admin-Key: <ADMIN_KEY>"

# Загрузить ключ расшифровки (42 пары → 6 типов × 14 баллов)
curl -X POST http://localhost:8000/polls/holland/<poll_id>/seed-scores \
  -H "X-Admin-Key: <ADMIN_KEY>"

Переменные окружения

Важно: перед деплоем в продакшн обязательно смените SECRET_KEY, ADMIN_KEY, DOCS_PASSWORD и пароли БД.

Переменная Описание Пример
APP_PATH Абсолютный путь к директории приложения внутри контейнера /app
DB_HOST Хост PostgreSQL postgres
DB_PORT Порт PostgreSQL 5432
DB_USER Пользователь БД postgres
DB_PASS Пароль БД postgres
DB_NAME Имя базы данных profi
REDIS_HOST Хост Redis redis
REDIS_PORT Порт Redis 6379
REDIS_DB Номер БД Redis 0
REDIS_PASSWORD Пароль Redis
SECRET_KEY Секрет для подписи JWT-токенов (min 32 символа) python -c "import secrets; print(secrets.token_hex(32))"
ADMIN_KEY Ключ для защищённых admin-эндпоинтов
DOCS_USERNAME Логин для доступа к /docs и /redoc admin
DOCS_PASSWORD Пароль для доступа к /docs и /redoc
API_URL Внутренний URL API для фронтенда (только Docker-сеть) http://api:8000

Файл docker-compose.yml содержит демо-значения — не используйте их в продакшн.


Структура проекта

api-copp/
├── main.py                  # точка входа FastAPI, регистрация роутеров
├── requirements.txt         # зависимости API
├── pyproject.toml           # метаданные проекта
├── Dockerfile.api           # образ API (python:3.14-slim)
├── Dockerfile.web           # образ фронтенда (python:3.11-slim + flet), порт 80
├── docker-compose.yml       # API (без портов) + Web (порт 80)
├── docker-compose_db.yml    # PostgreSQL + Redis
├── nginx.conf               # пример конфига nginx (для внешнего прокси)
│
├── bd/                      # слой доступа к данным
│   ├── __init__.py          # Settings (pydantic), make_engine()
│   └── tables/
│       ├── simple_base.py   # общий Base для SQLAlchemy
│       ├── users.py         # User
│       ├── organization.py  # Organization
│       ├── group.py         # Group
│       ├── poll.py          # Poll
│       ├── question.py      # Question
│       ├── choice.py        # Choice
│       ├── response.py      # Response, Answer
│       ├── scale.py         # ScaleDimension, ChoiceScore
│       └── radar_result.py  # RadarResult, RadarResultItem
│
├── route/                   # FastAPI-роутеры
│   ├── auth_utils.py        # JWT, bcrypt, require_admin_key
│   ├── auth.py              # /auth/register, /auth/login
│   ├── base.py              # /health, /version
│   ├── init_data_base.py    # /db/create-tables, /db/migrate
│   ├── users_crud.py        # CRUD пользователей
│   ├── groups_crud.py       # CRUD групп
│   ├── organizations_crud.py# CRUD организаций
│   ├── poll_crud.py         # CRUD опросов
│   ├── question_crud.py     # CRUD вопросов
│   ├── choice_crud.py       # CRUD вариантов ответов
│   ├── response_crud.py     # приём ответов + авто-расчёт радара
│   ├── holland_crud.py      # генерация теста Голланда + seed-scores
│   ├── glomshtok_crud.py    # генерация теста Гломштока
│   ├── klimov_crud.py       # генерация теста Климова
│   ├── scale_crud.py        # CRUD осей и расшифровок
│   ├── radar_crud.py        # расчёт и чтение радарных результатов
│   └── radar_svg_gen.py     # серверная генерация SVG-диаграмм
│
├── web/                     # Flet-фронтенд
│   ├── main.py              # точка входа Flet ASGI + применение темы
│   ├── router.py            # маршрутизация страниц
│   ├── api_client.py        # httpx-обёртки над API (API_URL из env)
│   ├── designer.py          # дизайн-система: colors, компоненты
│   ├── theme_manager.py     # менеджер тем, сохранение в client_storage
│   ├── radar_chart.py       # виджет RadarChart (flet.canvas)
│   ├── radar_svg.py         # генератор SVG (устаревший, не используется)
│   └── views/
│       ├── login_view.py
│       ├── reg_view.py
│       ├── user_view.py
│       ├── profile_view.py
│       ├── poll_view.py
│       ├── response_view.py
│       ├── my_tests_view.py
│       ├── take_test_view.py
│       └── content_users/
│           ├── main_page.py       # главная: «Все тесты» / «Мои тесты»
│           ├── poll_page.py       # прохождение теста
│           ├── response_page.py   # результат с паутинной диаграммой
│           ├── my_tests_page.py   # список пройденных тестов
│           ├── take_test_page.py  # список доступных тестов
│           └── profile_page.py    # редактирование профиля + выбор темы
│
└── data/                    # смонтированный том Docker (./data:/app/data)
    ├── postgres/            # данные PostgreSQL
    └── radar/               # сохранённые SVG-файлы диаграмм

База данных

Схема таблиц

organizations ──< groups ──< users
                              │
polls ──< questions ──< choices ──< choice_scores
  │                                     │
  └──< responses ──< answers            │
         │                              │
         └──< radar_results ──< radar_result_items
              (image_path)         └── (dimension_id FK → scale_dimensions)
                                               ↑
polls ──< scale_dimensions ──────────────────-─┘

Ключевые таблицы

Таблица Описание
users Испытуемые: username, hashed_password, first_name, last_name, FK на groups и organizations
polls Тест/опрос: title, description
questions Вопросы теста с type (single / multiple)
choices Варианты ответов к вопросам
responses Один факт прохождения теста (user + poll + timestamp)
answers Конкретные ответы внутри прохождения
scale_dimensions Оси радарной диаграммы: name, color, position, FK на polls
choice_scores Расшифровка: какой вариант ответа сколько баллов вносит в какую ось
radar_results Итоговый результат по осям + image_path (путь к SVG-файлу)
radar_result_items Снимок значений по каждой оси на момент расчёта

Миграции

Таблицы создаются через POST /db/create-tables. Для уже существующих БД применяются дополнительные ALTER-миграции через POST /db/migrate. Текущие миграции:

  • Добавление колонок username / hashed_password в users
  • Добавление колонки image_path в radar_results

API

Аутентификация

POST /auth/register      — регистрация (username ≥ 3 симв., password ≥ 8 симв.)
POST /auth/login         — получить JWT-токен (OAuth2 password flow)

JWT-токен передаётся заголовком Authorization: Bearer <token>. Admin-операции требуют заголовок X-Admin-Key.

Основные эндпоинты

# Тесты
GET    /polls                    — список тестов
POST   /polls                    — создать тест
GET    /polls/{id}               — тест с вопросами и вариантами
DELETE /polls/{id}               — удалить тест

# Вопросы / Варианты ответов
POST   /polls/{id}/questions
POST   /questions/{id}/choices

# Прохождение
POST   /polls/{poll_id}/responses           — сдать тест (автоматически считает радар)
GET    /polls/users/{user_id}/responses     — история пользователя
GET    /responses/{id}                      — конкретный результат

# Результаты (радар)
POST   /responses/{id}/radar/compute        — (пере)считать радарный результат
GET    /responses/{id}/radar                — данные для диаграммы
GET    /responses/{id}/radar/image          — SVG-файл диаграммы
GET    /users/{user_id}/radar-results       — история радарных результатов

# Оси и расшифровки
POST   /polls/{poll_id}/dimensions          — создать ось
GET    /polls/{poll_id}/dimensions          — список осей
PUT    /dimensions/{id}
DELETE /dimensions/{id}
POST   /dimensions/{id}/scores              — задать расшифровку варианта ответа
GET    /choices/{choice_id}/scores

# Готовые тесты (admin)
POST   /polls/holland                       — создать тест Голланда (42 пары)
POST   /polls/holland/{poll_id}/seed-scores — загрузить ключ расшифровки Голланда
POST   /polls/glomshtok                     — создать тест Гломштока
POST   /polls/klimov                        — создать тест Климова

# База данных (admin)
POST   /db/create-tables
POST   /db/migrate

# Служебные
GET    /health
GET    /version

Тест Голланда

Реализована модификация Г.В. Резапкиной теста профессиональной ориентации Дж. Голланда. 42 пары профессий, испытуемый выбирает предпочтительную в каждой паре.

Типы личности

Код Название Цвет
Р Реалистичный #FF8C00
И Интеллектуальный #4169E1
С Социальный #32CD32
К Конвенциональный #9370DB
П Предприимчивый #DC143C
А Артистический #FF69B4

Каждый тип получает ровно 14 вопросов (= 14 максимально возможных баллов). Результат — паутинная диаграмма с 6 осями.

Процедура загрузки

  1. POST /polls/holland — создаёт тест с 42 вопросами-парами
  2. POST /polls/holland/{poll_id}/seed-scores — создаёт 6 ScaleDimension и 84 ChoiceScore

Паутинная диаграмма

Как работает

При сдаче теста (POST /polls/{id}/responses) сервер автоматически:

  1. Суммирует баллы по каждой оси (ChoiceScore.score) для выбранных вариантов
  2. Сохраняет результат в radar_results + radar_result_items
  3. Генерирует SVG-файл и сохраняет в {APP_PATH}/data/radar/{result_id}.svg
  4. Записывает относительный путь в image_path — всё в одном commit()

Скачивание SVG

Фронтенд получает SVG-байты с API через внутреннюю Docker-сеть (api_client.get_radar_svg_bytes()), кодирует в data:application/octet-stream;base64,... и передаёт в page.launch_url(). Браузер скачивает файл напрямую из data URI — никаких запросов к API из браузера.

Генерация SVG

SVG генерируется чистым Python без зависимостей (route/radar_svg_gen.py):

  • 5 концентрических сеток
  • Подписи с именем типа и баллом
  • Размер холста: 520×520 пикселей

Фронтенд

Написан на Flet — Python-фреймворке поверх Flutter. Работает как ASGI-приложение через uvicorn на порту 80.

Маршруты

Путь Страница
/login Вход
/reg Регистрация
/user/{url_key} Главная страница пользователя
/take_test/{url_key} Список тестов для прохождения
/poll/{url_key}/{poll_id} Прохождение теста
/response/{url_key}/{response_id} Результат с диаграммой
/my_tests/{url_key} Мои результаты
/profile/{url_key} Профиль пользователя + выбор темы

{url_key} — одноразовый 32-символьный hex-ключ, генерируется при успешном входе (secrets.token_hex(16)). Обращение к защищённому маршруту с чужим ключом перенаправляет на /login.

Варианты ответов в тесте

Вместо стандартного ft.RadioGroup / ft.Radio (лейбл которого не переносится) используются кастомные кликабельные контейнеры (poll_page.py). При выборе варианта:

  • фон меняется с surface_altprimary_light
  • иконка меняется с RADIO_BUTTON_UNCHECKEDCHECK_CIRCLE

Дизайн-система

Все UI-компоненты сосредоточены в web/designer.py. Управление темой — в web/theme_manager.py.

Глобальный масштаб

SCALE: float = 1.7   # 1.0 = 100%, меняйте только эту константу
s(14)                # → round(14 * 1.7) = 24px

Цветовая палитра (стиль copp74.ru)

Имя HEX Применение
colors.primary #F85A40 Кнопки, акценты, логотип
colors.primary_dark #D94530 Hover-состояние кнопок
colors.primary_light #FFEAB7 Выбранный вариант ответа
colors.background #F4F6F8 Фон страниц
colors.surface #FFFFFF Карточки, формы
colors.surface_alt #F0F2F5 Невыбранные варианты, значки
colors.accent #FFC845 Жёлтый акцент
colors.info #007FBD Синие кнопки скачивания
colors.success #85C446 Зелёный (успех)
colors.text_primary #1E2A3A Основной текст
colors.text_secondary #5A6779 Вторичный текст, плейсхолдеры
colors.text_on_primary #FFFFFF Текст на оранжевых кнопках
colors.border #DDE3EA Границы полей

Компоненты

Класс Наследует Описание
CastomText ft.Text Текст с масштабированием
CastomTextField_input ft.TextField Поле ввода, белый фон, оранжевый фокус
CastomButton ft.Button Оранжевая кнопка с белым текстом
CastomIconButton ft.Button Синяя кнопка с иконкой
CastomDropdown ft.Dropdown Выпадающий список
CastomSwitch ft.Switch Переключатель
AlterDiaalogApproval ft.AlertDialog Диалог пользовательского соглашения

Темы интерфейса

Пользователь может выбрать тему в профиле. Тема сохраняется в client_storage браузера и восстанавливается при следующем входе.

ID Название Фон
default ЦОПП (orange) #F4F6F8
light Светлая #FFFFFF
dark_blue Синяя #E8F4FD
green Зелёная #EDF7E7

Деплой с внешним прокси

В репозитории есть готовый файл nginx.conf для случая, когда nginx развёрнут на отдельном сервере или уже управляет несколькими сайтами.

Минимальная конфигурация upstream в вашем nginx:

location / {
    proxy_pass         http://<server-ip>:80;
    proxy_set_header   Host              $host;
    proxy_set_header   X-Real-IP         $remote_addr;
    proxy_http_version 1.1;
    proxy_set_header   Upgrade    $http_upgrade;
    proxy_set_header   Connection "Upgrade";
}

Заголовки Upgrade / Connection обязательны — Flet использует WebSocket.


Известные ошибки и просчёты

1. Лишнее дублирование генерации SVG

Проблема: SVG-генератор написан дважды — route/radar_svg_gen.py (сервер) и web/radar_svg.py (клиент).

Решение: Кнопка «Сохранить» скачивает SVG через api_client.get_radar_svg_bytes(). web/radar_svg.py больше не используется, но файл остался — можно удалить при следующем рефакторинге.


2. Несовместимость Python 3.14 и psycopg исправлено

Проблема: pyproject.toml указывал psycopg>=3.3.3, но реально используется синхронный pg8000.

Решение: Зависимость заменена на pg8000>=1.29.0.


3. Нет транзакции при сохранении SVG исправлено

Проблема: image_path обновлялся отдельным commit() после основного — при падении между ними запись оставалась без пути к файлу.

Решение: SVG генерируется до коммита, image_path устанавливается до коммита. Один атомарный session.commit().


4. Перенос подписей в диаграмме по буквам исправлено

Решение: _W_LBL увеличен до 130, добавлен max_lines=2, no_wrap=False.


5. Текст вариантов ответов обрезался исправлено

Проблема: ft.Radio(label=...) в Flet не переносит текст метки — длинные варианты ответов обрезались.

Решение: Реализованы кастомные кликабельные контейнеры с ft.Text(expand=True, no_wrap=False).


6. Ошибка module 'designer' has no attribute 'clolors' исправлено

Проблема: Класс цветов был назван clolors с опечаткой. После переименования в colors все ссылки в 10+ файлах указывали на старое имя.

Решение: Класс переименован в colors, все ссылки во всех view-файлах обновлены.



Содержание


Архитектура

Пользователь (браузер)
       │
       │ :80
 [ Внешний прокси / nginx ]   ← необязателен для локальной сети
       │
       │ :80
  [ web — Flet ASGI ]         ← единственный публичный сервис
       │
       │ http://api:8000  (только внутри Docker-сети)
  [ api — FastAPI ]
       │              │
 [ PostgreSQL :5432 ] [ Redis :6379 ]

Важно: сервис api не публикует порты наружу — он доступен только контейнеру web внутри Docker-сети. Пользователь никогда не обращается к API напрямую.

Сетевая топология Docker: PostgreSQL и Redis запускаются отдельным compose-файлом docker-compose_db.yml и подключаются к той же Docker network.


Быстрый старт

1. Инфраструктура (PostgreSQL + Redis)

docker compose -f docker-compose_db.yml up -d

2. Приложение (API + Web)

docker compose up -d --build

После запуска:

Адрес Что
http://localhost Веб-интерфейс (Flet)
http://localhost:8000/docs Swagger UI — только с хоста сервера, порт не опубликован
http://localhost:8000/redoc ReDoc

В продакшне API недоступен снаружи. Для работы с /docs используйте SSH-туннель:

ssh -L 8000:localhost:8000 user@your-server

Доступ к документации — логин/пароль из переменных DOCS_USERNAME / DOCS_PASSWORD.

3. Инициализация БД

После первого запуска выполните (с хоста сервера или через туннель):

# Создать таблицы
curl -X POST http://localhost:8000/db/create-tables \
  -H "X-Admin-Key: <ADMIN_KEY>"

# Применить миграции (добавляет колонки, если БД уже была создана ранее)
curl -X POST http://localhost:8000/db/migrate \
  -H "X-Admin-Key: <ADMIN_KEY>"

4. Загрузка теста Голланда

# Создать тест (возвращает poll_id)
curl -X POST http://localhost:8000/polls/holland \
  -H "X-Admin-Key: <ADMIN_KEY>"

# Загрузить ключ расшифровки (42 пары → 6 типов × 14 баллов)
curl -X POST http://localhost:8000/polls/holland/<poll_id>/seed-scores \
  -H "X-Admin-Key: <ADMIN_KEY>"

Переменные окружения

Важно: перед деплоем в продакшн обязательно смените SECRET_KEY, ADMIN_KEY, DOCS_PASSWORD и пароли БД.

Переменная Описание Пример
APP_PATH Абсолютный путь к директории приложения внутри контейнера /app
DB_HOST Хост PostgreSQL postgres
DB_PORT Порт PostgreSQL 5432
DB_USER Пользователь БД postgres
DB_PASS Пароль БД postgres
DB_NAME Имя базы данных profi
REDIS_HOST Хост Redis redis
REDIS_PORT Порт Redis 6379
REDIS_DB Номер БД Redis 0
REDIS_PASSWORD Пароль Redis
SECRET_KEY Секрет для подписи JWT-токенов (min 32 символа) python -c "import secrets; print(secrets.token_hex(32))"
ADMIN_KEY Ключ для защищённых admin-эндпоинтов
DOCS_USERNAME Логин для доступа к /docs и /redoc admin
DOCS_PASSWORD Пароль для доступа к /docs и /redoc
API_URL Внутренний URL API для фронтенда (только Docker-сеть) http://api:8000

Файл docker-compose.yml содержит демо-значения — не используйте их в продакшн.


Структура проекта

api-copp/
├── main.py                  # точка входа FastAPI, регистрация роутеров
├── requirements.txt         # зависимости API
├── pyproject.toml           # метаданные проекта
├── Dockerfile.api           # образ API (python:3.14-slim)
├── Dockerfile.web           # образ фронтенда (python:3.11-slim + flet), порт 80
├── docker-compose.yml       # API (без портов) + Web (порт 80)
├── docker-compose_db.yml    # PostgreSQL + Redis
├── nginx.conf               # пример конфига nginx (для внешнего прокси)
│
├── bd/                      # слой доступа к данным
│   ├── __init__.py          # Settings (pydantic), make_engine()
│   └── tables/
│       ├── simple_base.py   # общий Base для SQLAlchemy
│       ├── users.py         # User
│       ├── organization.py  # Organization
│       ├── group.py         # Group
│       ├── poll.py          # Poll
│       ├── question.py      # Question
│       ├── choice.py        # Choice
│       ├── response.py      # Response, Answer
│       ├── scale.py         # ScaleDimension, ChoiceScore
│       └── radar_result.py  # RadarResult, RadarResultItem
│
├── route/                   # FastAPI-роутеры
│   ├── auth_utils.py        # JWT, bcrypt, require_admin_key
│   ├── auth.py              # /auth/register, /auth/login
│   ├── base.py              # /health, /version
│   ├── init_data_base.py    # /db/create-tables, /db/migrate
│   ├── users_crud.py        # CRUD пользователей
│   ├── groups_crud.py       # CRUD групп
│   ├── organizations_crud.py# CRUD организаций
│   ├── poll_crud.py         # CRUD опросов
│   ├── question_crud.py     # CRUD вопросов
│   ├── choice_crud.py       # CRUD вариантов ответов
│   ├── response_crud.py     # приём ответов + авто-расчёт радара
│   ├── holland_crud.py      # генерация теста Голланда + seed-scores
│   ├── scale_crud.py        # CRUD осей и расшифровок
│   ├── radar_crud.py        # расчёт и чтение радарных результатов
│   └── radar_svg_gen.py     # серверная генерация SVG-диаграмм
│
├── web/                     # Flet-фронтенд
│   ├── main.py              # точка входа Flet ASGI
│   ├── router.py            # маршрутизация страниц
│   ├── api_client.py        # httpx-обёртки над API (API_URL из env)
│   ├── designer.py          # компоненты дизайн-системы
│   ├── radar_chart.py       # виджет RadarChart (flet.canvas)
│   ├── radar_svg.py         # генератор SVG-data-URI (на клиенте, не используется)
│   └── views/
│       ├── login_view.py
│       ├── reg_view.py
│       ├── user_view.py
│       ├── profile_view.py
│       ├── poll_view.py
│       ├── response_view.py
│       ├── my_tests_view.py
│       ├── take_test_view.py
│       └── content_users/
│           ├── main_page.py       # главная: «Все тесты» / «Мои тесты»
│           ├── poll_page.py       # прохождение теста (кастомные кнопки выбора)
│           ├── response_page.py   # результат с паутинной диаграммой
│           ├── my_tests_page.py   # список пройденных тестов
│           ├── take_test_page.py  # список доступных тестов
│           └── profile_page.py    # редактирование профиля
│
└── data/                    # смонтированный том Docker (./data:/app/data)
    ├── postgres/            # данные PostgreSQL
    └── radar/               # сохранённые SVG-файлы диаграмм

База данных

Схема таблиц

organizations ──< groups ──< users
                              │
polls ──< questions ──< choices ──< choice_scores
  │                                     │
  └──< responses ──< answers            │
         │                              │
         └──< radar_results ──< radar_result_items
              (image_path)         └── (dimension_id FK → scale_dimensions)
                                               ↑
polls ──< scale_dimensions ──────────────────-─┘

Ключевые таблицы

Таблица Описание
users Испытуемые: username, hashed_password, first_name, last_name, FK на groups и organizations
polls Тест/опрос: title, description
questions Вопросы теста с type (single / multiple)
choices Варианты ответов к вопросам
responses Один факт прохождения теста (user + poll + timestamp)
answers Конкретные ответы внутри прохождения
scale_dimensions Оси радарной диаграммы: name, color, position, FK на polls
choice_scores Расшифровка: какой вариант ответа сколько баллов вносит в какую ось
radar_results Итоговый результат по осям + image_path (путь к SVG-файлу)
radar_result_items Снимок значений по каждой оси на момент расчёта

Миграции

Таблицы создаются через POST /db/create-tables. Для уже существующих БД применяются дополнительные ALTER-миграции через POST /db/migrate. Текущие миграции:

  • Добавление колонок username / hashed_password в users
  • Добавление колонки image_path в radar_results

API

Аутентификация

POST /auth/register      — регистрация (username ≥ 3 симв., password ≥ 8 симв.)
POST /auth/login         — получить JWT-токен (OAuth2 password flow)

JWT-токен передаётся заголовком Authorization: Bearer <token>. Admin-операции требуют заголовок X-Admin-Key.

Основные эндпоинты

# Тесты
GET    /polls                    — список тестов
POST   /polls                    — создать тест
GET    /polls/{id}               — тест с вопросами и вариантами
DELETE /polls/{id}               — удалить тест

# Вопросы / Варианты ответов
POST   /polls/{id}/questions
POST   /questions/{id}/choices

# Прохождение
POST   /polls/{poll_id}/responses           — сдать тест (автоматически считает радар)
GET    /polls/users/{user_id}/responses     — история пользователя
GET    /responses/{id}                      — конкретный результат

# Результаты (радар)
POST   /responses/{id}/radar/compute        — (пере)считать радарный результат
GET    /responses/{id}/radar                — данные для диаграммы
GET    /responses/{id}/radar/image          — SVG-файл диаграммы
GET    /users/{user_id}/radar-results       — история радарных результатов

# Оси и расшифровки
POST   /polls/{poll_id}/dimensions          — создать ось
GET    /polls/{poll_id}/dimensions          — список осей
PUT    /dimensions/{id}
DELETE /dimensions/{id}
POST   /dimensions/{id}/scores              — задать расшифровку варианта ответа
GET    /choices/{choice_id}/scores

# Тест Голланда
POST   /polls/holland                       — создать тест с 42 парами
POST   /polls/holland/{poll_id}/seed-scores — загрузить ключ расшифровки

# База данных (admin)
POST   /db/create-tables
POST   /db/migrate

# Служебные
GET    /health
GET    /version

Тест Голланда

Реализована модификация Г.В. Резапкиной теста профессиональной ориентации Дж. Голланда. 42 пары профессий, испытуемый выбирает предпочтительную в каждой паре.

Типы личности

Код Название Цвет
Р Реалистичный #FF8C00
И Интеллектуальный #4169E1
С Социальный #32CD32
К Конвенциональный #9370DB
П Предприимчивый #DC143C
А Артистический #FF69B4

Каждый тип получает ровно 14 вопросов (= 14 максимально возможных баллов). Результат — паутинная диаграмма с 6 осями.

Процедура загрузки

  1. POST /polls/holland — создаёт тест с 42 вопросами-парами
  2. POST /polls/holland/{poll_id}/seed-scores — создаёт 6 ScaleDimension и 84 ChoiceScore

Паутинная диаграмма

Как работает

При сдаче теста (POST /polls/{id}/responses) сервер автоматически:

  1. Суммирует баллы по каждой оси (ChoiceScore.score) для выбранных вариантов
  2. Сохраняет результат в radar_results + radar_result_items
  3. Генерирует SVG-файл и сохраняет в {APP_PATH}/data/radar/{result_id}.svg
  4. Записывает относительный путь в image_path — всё в одном commit()

Скачивание SVG

Фронтенд получает SVG-байты с API через внутреннюю Docker-сеть (api_client.get_radar_svg_bytes()), кодирует в data:application/octet-stream;base64,... и передаёт в page.launch_url(). Браузер скачивает файл напрямую из data URI — никаких запросов к API из браузера, никакой зависимости от публичности API.

Генерация SVG

SVG генерируется чистым Python без зависимостей (route/radar_svg_gen.py):

  • Фон #2F184B, полигон rgba(155,114,207,0.55)
  • 5 концентрических сеток
  • Подписи с именем типа и баллом
  • Размер холста: 520×520 пикселей

Фронтенд

Написан на Flet — Python-фреймворке поверх Flutter. Работает как ASGI-приложение через uvicorn на порту 80.

Адаптация под мобильные устройства

Интерфейс адаптирован для экранов с малым горизонтальным разрешением:

  • Формы входа и регистрации растягиваются на всю ширину экрана (без фиксированной ширины 360px)
  • Карточки навигации («Все тесты» / «Мои тесты») переносятся на следующую строку при нехватке места (ft.Row(wrap=True))
  • Варианты ответов в тесте реализованы кастомными контейнерами вместо ft.Radio — текст длинных вариантов переносится на следующую строку
  • Заголовок страницы пользователя не выталкивает кнопку профиля

Варианты ответов в тесте

Вместо стандартного ft.RadioGroup / ft.Radio (лейбл которого не переносится) используются кастомные кликабельные контейнеры (poll_page.py: _build_choice_group). При выборе варианта:

  • фон меняется с laer3laer1
  • появляется белая рамка
  • иконка меняется с RADIO_BUTTON_UNCHECKEDCHECK_CIRCLE

Маршруты

Путь Страница
/login Вход
/reg Регистрация
/user/{url_key} Главная страница пользователя
/take_test/{url_key} Список тестов для прохождения
/poll/{url_key}/{poll_id} Прохождение теста
/response/{url_key}/{response_id} Результат с диаграммой
/my_tests/{url_key} Мои результаты
/profile/{url_key} Профиль пользователя

{url_key} — одноразовый 32-символьный hex-ключ, генерируется при успешном входе (secrets.token_hex(16)). Обращение к защищённому маршруту с чужим ключом перенаправляет на /login.


Деплой с внешним прокси

В репозитории есть готовый файл nginx.conf для случая, когда nginx развёрнут на отдельном сервере или уже управляет несколькими сайтами.

Минимальная конфигурация upstream в вашем nginx:

location / {
    proxy_pass         http://<server-ip>:80;
    proxy_set_header   Host              $host;
    proxy_set_header   X-Real-IP         $remote_addr;
    proxy_http_version 1.1;
    proxy_set_header   Upgrade    $http_upgrade;
    proxy_set_header   Connection "Upgrade";
}

Заголовки Upgrade / Connection обязательны — Flet использует WebSocket.


Известные ошибки и просчёты

1. Лишнее дублирование генерации SVG

Проблема: SVG-генератор написан дважды — route/radar_svg_gen.py (сервер) и web/radar_svg.py (клиент).

Решение: Кнопка «Сохранить» скачивает SVG через api_client.get_radar_svg_bytes(). web/radar_svg.py больше не используется, но файл остался — можно удалить при следующем рефакторинге.


2. Несовместимость Python 3.14 и psycopg исправлено

Проблема: pyproject.toml указывал psycopg>=3.3.3, но реально используется синхронный pg8000.

Решение: Зависимость заменена на pg8000>=1.29.0.


3. Нет транзакции при сохранении SVG исправлено

Проблема: image_path обновлялся отдельным commit() после основного — при падении между ними запись оставалась без пути к файлу.

Решение: SVG генерируется до коммита, image_path устанавливается до коммита. Один атомарный session.commit().


4. Перенос подписей в диаграмме по буквам исправлено

Решение: _W_LBL увеличен до 130, добавлен max_lines=2, no_wrap=False.


5. Текст вариантов ответов обрезался исправлено

Проблема: ft.Radio(label=...) в Flet не переносит текст метки — длинные варианты ответов обрезались.

Решение: Реализованы кастомные кликабельные контейнеры с ft.Text(expand=True, no_wrap=False).


6. Браузер не может обратиться к http://api:8000 исправлено

Проблема: Внутреннее Docker-имя api недоступно из браузера пользователя.

Решение: Фронтенд скачивает SVG через httpx (внутри Docker-сети) и передаёт браузеру как data:application/octet-stream;base64,....


7. image_path добавлен после создания таблиц исправлено

Решение: Добавлена явная миграция:

ALTER TABLE radar_results ADD COLUMN IF NOT EXISTS image_path VARCHAR(512);

8. Документация API доступна без авторизации исправлено

Решение: Стандартные маршруты /docs, /redoc, /openapi.json отключены. Зарегистрированы собственные с HTTP Basic Auth через secrets.compare_digest.


Содержание


Архитектура

┌─────────────────────────────────────────────┐
│  Браузер пользователя  :80                  │
│  (Flet Web App)                             │
└────────────────────┬────────────────────────┘
                     │ HTTP / REST
┌────────────────────▼────────────────────────┐
│  FastAPI API  :8000                         │
│  - аутентификация (JWT)                     │
│  - управление тестами                       │
│  - сбор ответов                             │
│  - расчёт радарных результатов              │
│  - генерация SVG-диаграмм                   │
└──────┬─────────────────────┬────────────────┘
       │                     │
┌──────▼──────┐     ┌────────▼────────┐
│ PostgreSQL  │     │     Redis       │
│   :5432     │     │     :6379       │
│ (основная   │     │ (сессии /       │
│  БД)        │     │  кэш)           │
└─────────────┘     └─────────────────┘

Сетевая топология Docker: все три сервиса (api, web, postgres, redis) работают в одной Docker-сети. Фронтенд (web) обращается к API по внутреннему имени http://api:8000. PostgreSQL и Redis запускаются отдельным compose-файлом docker-compose_db.yml.


Быстрый старт

1. Инфраструктура (PostgreSQL + Redis)

docker compose -f docker-compose_db.yml up -d

2. Приложение (API + Web)

docker compose up -d --build

После запуска:

Адрес Что
http://localhost Веб-интерфейс (Flet)
http://localhost:8000/docs Swagger UI (HTTP Basic Auth)
http://localhost:8000/redoc ReDoc

Доступ к документации — логин/пароль из переменных DOCS_USERNAME / DOCS_PASSWORD.

3. Инициализация БД

После первого запуска выполните:

# Создать таблицы
curl -X POST http://localhost:8000/db/create-tables \
  -H "X-Admin-Key: <ADMIN_KEY>"

# Применить миграции (добавляет колонки, если БД уже была создана ранее)
curl -X POST http://localhost:8000/db/migrate \
  -H "X-Admin-Key: <ADMIN_KEY>"

4. Загрузка теста Голланда

# Создать тест (возвращает poll_id)
curl -X POST http://localhost:8000/polls/holland \
  -H "X-Admin-Key: <ADMIN_KEY>"

# Загрузить ключ расшифровки (42 пары → 6 типов × 14 баллов)
curl -X POST http://localhost:8000/polls/holland/<poll_id>/seed-scores \
  -H "X-Admin-Key: <ADMIN_KEY>"

Переменные окружения

Важно: перед деплоем в продакшн обязательно смените SECRET_KEY, ADMIN_KEY, DOCS_PASSWORD и пароли БД.

Переменная Описание Пример
APP_PATH Абсолютный путь к директории приложения внутри контейнера /home/user/api-copp
DB_HOST Хост PostgreSQL postgres
DB_PORT Порт PostgreSQL 5432
DB_USER Пользователь БД postgres
DB_PASS Пароль БД postgres
DB_NAME Имя базы данных profi
REDIS_HOST Хост Redis redis
REDIS_PORT Порт Redis 6379
REDIS_DB Номер БД Redis 0
REDIS_PASSWORD Пароль Redis
SECRET_KEY Секрет для подписи JWT-токенов (min 32 символа) python -c "import secrets; print(secrets.token_hex(32))"
ADMIN_KEY Ключ для защищённых admin-эндпоинтов
DOCS_USERNAME Логин для доступа к /docs и /redoc admin
DOCS_PASSWORD Пароль для доступа к /docs и /redoc
API_URL Внутренний URL API для фронтенда http://api:8000

Файлы docker-compose.yml содержат демо-значения — не используйте их в продакшн.


Структура проекта

api-copp/
├── main.py                  # точка входа FastAPI, регистрация роутеров
├── requirements.txt         # зависимости API
├── pyproject.toml           # метаданные проекта
├── Dockerfile.api           # образ API (python:3.14-slim)
├── Dockerfile.web           # образ фронтенда (python:3.11-slim + flet)
├── docker-compose.yml       # API + Web
├── docker-compose_db.yml    # PostgreSQL + Redis
├── docker-compose_web.yml   # отдельный запуск только Web
│
├── bd/                      # слой доступа к данным
│   ├── __init__.py          # Settings (pydantic), make_engine()
│   └── tables/
│       ├── simple_base.py   # общий Base для SQLAlchemy
│       ├── users.py         # User
│       ├── organization.py  # Organization
│       ├── group.py         # Group
│       ├── poll.py          # Poll
│       ├── question.py      # Question
│       ├── choice.py        # Choice
│       ├── response.py      # Response, Answer
│       ├── scale.py         # ScaleDimension, ChoiceScore
│       └── radar_result.py  # RadarResult, RadarResultItem
│
├── route/                   # FastAPI-роутеры
│   ├── auth_utils.py        # JWT, bcrypt, require_admin_key
│   ├── auth.py              # /auth/register, /auth/login
│   ├── base.py              # /health, /version
│   ├── init_data_base.py    # /db/create-tables, /db/migrate
│   ├── users_crud.py        # CRUD пользователей
│   ├── groups_crud.py       # CRUD групп
│   ├── organizations_crud.py# CRUD организаций
│   ├── poll_crud.py         # CRUD опросов
│   ├── question_crud.py     # CRUD вопросов
│   ├── choice_crud.py       # CRUD вариантов ответов
│   ├── response_crud.py     # приём ответов + авто-расчёт радара
│   ├── holland_crud.py      # генерация теста Голланда + seed-scores
│   ├── scale_crud.py        # CRUD осей и расшифровок
│   ├── radar_crud.py        # расчёт и чтение радарных результатов
│   └── radar_svg_gen.py     # серверная генерация SVG-диаграмм
│
├── web/                     # Flet-фронтенд
│   ├── main.py              # точка входа Flet ASGI
│   ├── router.py            # маршрутизация страниц
│   ├── api_client.py        # httpx-обёртки над API
│   ├── designer.py          # компоненты дизайн-системы
│   ├── radar_chart.py       # виджет RadarChart (flet.canvas)
│   ├── radar_svg.py         # генератор SVG-data-URI (на клиенте)
│   └── views/
│       └── content_users/
│           ├── poll_page.py       # прохождение теста + диаграмма сразу после
│           ├── response_page.py   # страница результата с диаграммой
│           ├── my_tests_page.py   # список пройденных тестов
│           └── ...
│
└── data/                    # смонтированный том Docker (./data:/app/data)
    ├── postgres/            # данные PostgreSQL
    └── radar/               # сохранённые SVG-файлы диаграмм (создаётся автоматически)

База данных

Схема таблиц

organizations ──< groups ──< users
                              │
polls ──< questions ──< choices ──< choice_scores
  │                                     │
  └──< responses ──< answers            │
         │                              │
         └──< radar_results ──< radar_result_items
              (image_path)         └── (dimension_id FK → scale_dimensions)
                                               ↑
polls ──< scale_dimensions ──────────────────-─┘

Ключевые таблицы

Таблица Описание
users Испытуемые: username, hashed_password, first_name, last_name, FK на groups и organizations
polls Тест/опрос: title, description
questions Вопросы теста с type (single / multiple)
choices Варианты ответов к вопросам
responses Один факт прохождения теста (user + poll + timestamp)
answers Конкретные ответы внутри прохождения
scale_dimensions Оси радарной диаграммы: name, color, position, FK на polls
choice_scores Расшифровка: какой вариант ответа сколько баллов вносит в какую ось
radar_results Итоговый результат по осям + image_path (путь к SVG-файлу)
radar_result_items Снимок значений по каждой оси на момент расчёта

Миграции

Таблицы создаются через POST /db/create-tables. Для уже существующих БД применяются дополнительные ALTER-миграции через POST /db/migrate. Текущие миграции:

  • Добавление колонок username / hashed_password в users
  • Добавление колонки image_path в radar_results

API

Аутентификация

POST /auth/register      — регистрация (username ≥ 3 симв., password ≥ 8 симв.)
POST /auth/login         — получить JWT-токен (OAuth2 password flow)

JWT-токен передаётся заголовком Authorization: Bearer <token>. Admin-операции требуют заголовок X-Admin-Key.

Основные эндпоинты

# Тесты
GET    /polls                    — список тестов
POST   /polls                    — создать тест
GET    /polls/{id}               — тест с вопросами и вариантами
DELETE /polls/{id}               — удалить тест

# Вопросы / Варианты ответов
POST   /polls/{id}/questions
POST   /questions/{id}/choices

# Прохождение
POST   /polls/{poll_id}/responses           — сдать тест (автоматически считает радар)
GET    /polls/users/{user_id}/responses     — история пользователя
GET    /responses/{id}                      — конкретный результат

# Результаты (радар)
POST   /responses/{id}/radar/compute        — (пере)считать радарный результат
GET    /responses/{id}/radar                — данные для диаграммы
GET    /responses/{id}/radar/image          — SVG-файл диаграммы
GET    /users/{user_id}/radar-results       — история радарных результатов

# Оси и расшифровки
POST   /polls/{poll_id}/dimensions          — создать ось
GET    /polls/{poll_id}/dimensions          — список осей
PUT    /dimensions/{id}
DELETE /dimensions/{id}
POST   /dimensions/{id}/scores              — задать расшифровку варианта ответа
GET    /choices/{choice_id}/scores

# Тест Голланда
POST   /polls/holland                       — создать тест с 42 парами
POST   /polls/holland/{poll_id}/seed-scores — загрузить ключ расшифровки

# База данных (admin)
POST   /db/create-tables
POST   /db/migrate

# Служебные
GET    /health
GET    /version

Тест Голланда

Реализована модификация Г.В. Резапкиной теста профессиональной ориентации Дж. Голланда. 42 пары профессий, испытуемый выбирает предпочтительную в каждой паре.

Типы личности

Код Название Цвет
Р Реалистичный #FF8C00
И Интеллектуальный #4169E1
С Социальный #32CD32
К Конвенциональный #9370DB
П Предприимчивый #DC143C
А Артистический #FF69B4

Каждый тип получает ровно 14 вопросов (= 14 максимально возможных баллов). Результат — паутинная диаграмма с 6 осями.

Процедура загрузки

  1. POST /polls/holland — создаёт тест с 42 вопросами-парами
  2. POST /polls/holland/{poll_id}/seed-scores — создаёт 6 ScaleDimension и 84 ChoiceScore (по 2 на каждую пару: вариант A → тип X, вариант B → тип Y)

Паутинная диаграмма

Как работает

При сдаче теста (POST /polls/{id}/responses) сервер автоматически:

  1. Суммирует баллы по каждой оси (ChoiceScore.score) для выбранных вариантов
  2. Сохраняет результат в radar_results + radar_result_items
  3. Генерирует SVG-файл и сохраняет в {APP_PATH}/data/radar/{result_id}.svg
  4. Записывает относительный путь data/radar/{result_id}.svg в колонку image_path

Скачивание SVG

Фронтенд скачивает SVG с API через внутреннюю сеть (GET /responses/{id}/radar/image), кодирует в base64 data URI и открывает в браузере как data:image/svg+xml;base64,... — файл сохраняется на устройство пользователя без необходимости публичного URL к серверу.

Генерация SVG

SVG генерируется чистым Python без зависимостей (route/radar_svg_gen.py). Серверный SVG идентичен клиентскому (web/radar_svg.py) по разметке, но предназначен для долгосрочного хранения. Параметры:

  • Фон #2F184B, полигон rgba(155,114,207,0.55)
  • 5 концентрических сеток
  • Подписи с именем типа и баллом
  • Размер холста: 520×520 пикселей

Фронтенд

Написан на Flet — Python-фреймворке поверх Flutter. Работает как ASGI-приложение через uvicorn, отдаётся браузеру как веб-приложение.

Маршруты

Путь Страница
/login Вход
/reg Регистрация
/user/{url_key} Главная страница пользователя
/take_test/{url_key} Список тестов для прохождения
/poll/{url_key}/{poll_id} Прохождение теста
/response/{url_key}/{response_id} Результат с диаграммой
/my_tests/{url_key} Мои результаты
/profile/{url_key} Профиль пользователя

{url_key} — одноразовый 32-символьный hex-ключ, генерируется при успешном входе (secrets.token_hex(16)) и сохраняется в сессии. Обращение к защищённому маршруту с чужим или отсутствующим ключом автоматически сбрасывает сессию и перенаправляет на /login.

Диаграмма после теста

После сдачи теста пользователь сразу видит паутинную диаграмму (виджет RadarChart) и кнопку «Сохранить диаграмму», которая загружает SVG с сервера и открывает его в новой вкладке для сохранения.


Известные ошибки и просчёты

1. Лишнее дублирование генерации SVG

Проблема: SVG-генератор написан дважды — route/radar_svg_gen.py (сервер) и web/radar_svg.py (клиент). Кнопка «Сохранить» изначально создавала SVG на клиенте заново вместо использования уже сохранённого серверного файла.

Решение: Кнопка «Сохранить» теперь скачивает SVG через api_client.get_radar_svg_bytes(), а не регенерирует его. Серверный файл — источник истины.

Оставшийся долг: web/radar_svg.py больше не используется кнопками сохранения, но файл остался. Можно удалить при следующем рефакторинге.


2. Несовместимость Python 3.14 и psycopg исправлено

Проблема: pyproject.toml указывал requires-python = ">=3.14" и зависимость psycopg>=3.3.3, однако Dockerfile.api использует python:3.14-slim. psycopg (асинхронный адаптер) на 3.14 может не иметь собранных wheels. В requirements.txt вместо psycopg используется pg8000==1.29.0 — синхронный pure-Python драйвер.

Решение: Все запросы к БД синхронные (make_engine() возвращает синхронный create_engine с postgresql+pg8000://...). Фактически psycopg не используется. Ошибочная зависимость psycopg заменена на pg8000>=1.29.0 в pyproject.toml.


3. Нет транзакции при сохранении SVG исправлено

Проблема: После основного session.commit() (сохранение RadarResult и RadarResultItem) шло отдельное обновление result.image_path. Если процесс падал между первым и вторым commit — в БД оставалась запись без image_path, хотя файл уже существовал на диске.

Последствие: При повторном прохождении теста пересчёт (compute_radar) удалял старый RadarResult и создавал новый, что восстанавливало корректность. Но исторический файл .svg «осиротевал» на диске.

Решение: items_data теперь строится непосредственно из scores_map/dimensions (данные уже в памяти после flush()), SVG генерируется до коммита, result.image_path устанавливается до коммита. Итого один session.commit() атомарно сохраняет RadarResult, RadarResultItem и путь к SVG.


4. Перенос подписей в диаграмме по буквам (устранено)

Проблема: В виджете RadarChart (web/radar_chart.py) подписи осей переносились по буквам из-за слишком малой ширины контейнера (_W_LBL=90).

Решение: Увеличено до _W_LBL=130, добавлен max_lines=2, no_wrap=False — перенос только по словам.


5. Первоначально неверные пары профессий

Проблема: В первой версии holland_crud.py использовались произвольные пары профессий, не соответствующие оригинальной методике. Ключ расшифровки (CHOICE_KEY) не был реализован — загрузка в БД не работала.

Решение: Заменены на 42 официальные пары из модификации Резапкиной с корректным CHOICE_KEY — каждый из 6 типов (Р/И/С/К/П/А) получает ровно 14 баллов-вопросов.


6. Браузер не может напрямую обратиться к http://api:8000

Проблема: Внутреннее Docker-имя api недоступно из браузера пользователя. Если бы кнопка «Сохранить» формировала URL вида http://api:8000/responses/.../radar/image и передавала его в page.launch_url() — браузер получал бы ошибку DNS.

Решение: Фронтенд скачивает SVG-байты с API через httpx (внутренняя сеть), конвертирует в base64 data URI и передаёт его в page.launch_url(). Браузер открывает data URI напрямую, без обращения к серверу.


7. image_path добавлен после создания таблиц (требует миграции)

Проблема: Колонка image_path была добавлена в RadarResult после того, как таблицы уже создавались через POST /db/create-tables. create_all не изменяет существующие таблицы.

Решение: Добавлена явная миграция в route/init_data_base.py:

ALTER TABLE radar_results ADD COLUMN IF NOT EXISTS image_path VARCHAR(512);

Применяется вызовом POST /db/migrate.


8. Документация API доступна без авторизации по умолчанию у FastAPI (устранено)

Проблема: FastAPI по умолчанию открывает /docs и /openapi.json без каких-либо ограничений.

Решение: docs_url=None, openapi_url=None, redoc_url=None — стандартные маршруты отключены. Вместо них зарегистрированы собственные /docs, /redoc, /openapi.json с HTTP Basic Auth через secrets.compare_digest (защита от timing-атак).

Description
No description provided
Readme 1.1 MiB
Languages
Python 59.5%
TypeScript 33.9%
CSS 6.1%
Dockerfile 0.3%
HTML 0.2%