- Python 75.2%
- TypeScript 19%
- CSS 4.6%
- Dockerfile 0.5%
- HTML 0.4%
- Other 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
student_id — ключ от аккаунта: зная его, можно привязать своё устройство и действовать от чужого имени (и на бирже пар тоже). API отдавал его всем — в авторах тем и в списках участников. Теперь наружу уходит случайный public_id; поле в списках участников осталось student_id, чтобы выпущенное расширение работало без обновления. Плюс то, что сервер не может проверить, настоящий ли студент: - student_id только в формате UUID; - лимиты по IP на новых студентов, публикации, комнаты, неверные коды, жалобы и входы в админку; - квота места на студента и запас свободного места на диске. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
| admin | ||
| alembic | ||
| app | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| alembic.ini | ||
| docker-compose.yml | ||
| Dockerfile | ||
| pyproject.toml | ||
| README.md | ||
| requirements-dev.txt | ||
| requirements.txt | ||
lms-workshop
Мастерская тем для расширения CU LMS Enhancer: студенты публикуют своё оформление LMS — палитру и CSS, логотип, фоны, обложки и названия курсов, — а другие примеряют и ставят его в пару кликов.
Здесь лежат API, которым пользуется страница мастерской в расширении, и
админка модерации (admin/, React), которую отдаёт тот же сервер по
/admin/.
Комнаты
Темы публикуются в комнаты. Общая комната с кодом PUBLIC создаётся
миграцией, в ней состоят все без вступления. Всё, что туда публикуют, —
и новая тема, и каждая её следующая версия — сначала попадает в очередь
админки. Пока версию не одобрили, её видит только автор, а остальные видят и
ставят прежнюю: тема не пропадает из витрины на время проверки обновления.
Приватную комнату заводит любой студент: название своё, код — шесть
случайных символов без I, O, 0 и 1 (их путают на слух, и заодно код никогда
не совпадёт с PUBLIC). Войти можно только по коду, модерации нет — за
порядком следит владелец: удаляет чужие темы, выгоняет участников, меняет
код. Выгнать значит запретить вход: код у человека остался, и без запрета
он вернулся бы следующим запросом. Неверных кодов студент может ввести 20 в
час — кодов около миллиарда, но давать их перебирать всё равно незачем.
Темы и картинки
Тема — это профиль настроек расширения (формат описан в PROFILE-FORMAT.md) плюс название, описание, обложка и до шести скриншотов. У темы есть версии: название, описание и картинки у каждой свои, лайки, установки и жалобы — общие.
Картинки в профиле приходят data:-URL'ами, отчего версия весит мегабайты.
Сервер вынимает каждую в файл с именем по sha256 содержимого, а в профиле
оставляет workshop-asset:<sha256>. Одинаковая обложка в пяти версиях лежит
на диске один раз, а файлы, на которые больше не ссылается ни одна версия,
удаляются вместе с темой. Отдаются они без авторизации — <img> заголовков
не шлёт, а хеш не угадать, не имея самой картинки — и с
Content-Security-Policy: sandbox: SVG-логотип со скриптом, открытый
отдельной вкладкой, иначе выполнился бы на домене админки.
Тип картинки определяется по первым байтам, а не по тому, что прислал
клиент. Профиль со заведомыми секретами (akh_token, ключи устройств)
отвергается: расширение само их не выгружает, но в общую комнату профиль
уходит ко всем, и изменённый клиент не должен иметь возможности это
сломать. Остальные ключи сервер не проверяет — что допустимо, знает только
расширение своей версии, и оно проверяет профиль при установке.
Авторизация
Студенты — та же схема, что в
lms-swap-backend:
расширение генерирует случайный ключ устройства, POST /api/v1/register
привязывает к нему student_id из LMS, дальше запросы идут с заголовками
Authorization: Bearer <device_key> и X-Student-Id. Сильные и слабые
стороны те же, они подробно разобраны в README биржи.
Главная слабость — секретом здесь работает сам student_id: кто его знает,
тот привяжет своё устройство и будет публиковать, удалять темы и управлять
комнатами от чужого имени. А ключ тот же, что у биржи пар, так что заодно и
там. Поэтому настоящий student_id наружу не уходит никогда — ни в
авторах тем, ни в списках участников и выгнанных, ни в админке. Остальным
студент виден по public_id — случайному UUID, который ничего не открывает.
Поле в списках участников по-прежнему называется student_id (так его ждёт
выпущенное расширение), но в нём public_id, и ручки «выгнать» и «пустить
обратно» принимают именно его. Свой настоящий id студент получает только в
ответе /register и /me. Проверяет это tests/test_security.py.
Что студент настоящий, сервер проверить не может: выдуманный UUID ничем не
хуже. Поэтому лимиты «на студента» дублируются лимитами на IP
(app/limits.py, значения — в app/config.py): новые аккаунты, публикации,
комнаты, неверные коды комнат, жалобы и неудачные входы в админку. Лимиты по
IP взяты с запасом — за одним адресом сидит весь кампусный Wi-Fi. Счётчики
живут в памяти процесса и обнуляются при перезапуске. Место ограничено
квотой на студента (STUDENT_QUOTA_BYTES, 200 МБ) и запасом на диске
(MIN_FREE_DISK_BYTES, 2 ГБ): ниже него публикации закрываются, потому что
диск у сервера общий с другими сервисами.
Из LMS сюда не попадает ничего, кроме student_id. Ник студент выбирает
сам — его видят под темами и в списке участников комнаты.
Админ один: логин и пароль из .env. Токен — подписанная HMAC строка со
сроком жизни, сессии в базе ради одного человека не хранятся. Без
ADMIN_PASSWORD в админку не пускает никого.
Модерация
Админка показывает очередь версий из общей комнаты. У каждой — картинки,
описание, что нового, и профиль целиком: палитра образцами цветов, свой CSS,
все картинки и названия курсов. Если CSS ходит наружу (url() на чужой
адрес, @import), админка предупреждает: такой запрос видит адреса всех,
кто поставит тему. Для обновления рядом показана опубликованная версия.
Отклонить можно только с причиной — её увидит автор. Жалобы на темы приходят туда же; снять тему значит спрятать её из комнаты с причиной для автора, это обратимо. Снятие закрывает все открытые жалобы на тему разом.
Запуск
cp .env.example .env # задать ADMIN_PASSWORD и ADMIN_SECRET
docker compose up --build
API поднимется на http://localhost:8000, доки — на /docs, админка — на
/admin/. Миграции применяются при старте контейнера. Картинки лежат в
volume assets (/data в контейнере): в базе только их хеши, так что
потерять volume значит потерять все обложки и картинки тем.
Локально без докера:
python -m venv .venv
.venv/bin/pip install -r requirements-dev.txt
export DATABASE_URL=sqlite:///./ws.db DATA_DIR=./data ADMIN_PASSWORD=secret
.venv/bin/alembic upgrade head
.venv/bin/uvicorn app.main:app --reload
Админка в разработке — отдельным vite с прокси /api на сервер:
cd admin
bun install
bun run dev # http://localhost:5173/admin/
bun run build # в admin/dist, оттуда её отдаёт сервер
Тесты гоняются на SQLite во временной папке:
.venv/bin/pytest
Расширение ходит на https://lms.workshop.cu3rd.ru; направить его на
локальный сервер можно ключом workshopBackendUrl в storage.local.
Сквозной тест расширения против живого сервера — tests/workshop.test.ts в
lms-extension.
Деплой
Наружу сервис отдаёт системный Caddy, контейнер слушает только localhost
(API_BIND). Порт — API_PORT из .env; рядом с биржей на 8085 удобно
взять 8086. Блок для Caddy:
lms.workshop.cu3rd.ru {
reverse_proxy 127.0.0.1:8086
}
CORS выставляет само приложение. Тело запроса публикации — до 30 МБ (профиль с картинками внутри), Caddy по умолчанию его не ограничивает.
Эндпоинты
Все студенческие, кроме /health, /register и /assets, требуют
заголовков авторизации.
| Метод | Путь | Что делает |
|---|---|---|
POST |
/api/v1/register |
Привязать ключ устройства к студенту |
GET |
/api/v1/me, PUT /api/v1/me |
Профиль и смена ника |
GET |
/api/v1/rooms |
Общая комната и все, где состоишь |
POST |
/api/v1/rooms |
Создать комнату |
POST |
/api/v1/rooms/join |
Войти по коду |
PATCH |
/api/v1/rooms/{id} |
Переименовать (владелец) |
POST |
/api/v1/rooms/{id}/code |
Новый код (владелец) |
DELETE |
/api/v1/rooms/{id} |
Удалить комнату со всеми темами (владелец) |
POST |
/api/v1/rooms/{id}/leave |
Выйти |
GET |
/api/v1/rooms/{id}/members |
Участники |
DELETE |
/api/v1/rooms/{id}/members/{sid} |
Выгнать; ?delete_themes=true — вместе с темами |
GET |
/api/v1/rooms/{id}/bans, DELETE …/bans/{sid} |
Выгнанные и снятие запрета (владелец) |
GET |
/api/v1/rooms/{id}/themes?sort=&q= |
Витрина: new, popular, likes, поиск |
POST |
/api/v1/rooms/{id}/themes |
Опубликовать тему (multipart) |
GET |
/api/v1/themes/{id} |
Тема с версиями |
DELETE |
/api/v1/themes/{id} |
Удалить (автор или владелец приватной комнаты) |
POST |
/api/v1/themes/{id}/versions |
Новая версия (автор, multipart) |
DELETE |
/api/v1/themes/{id}/versions/{vid} |
Отозвать неопубликованную версию |
GET |
/api/v1/themes/{id}/versions/{vid}/payload |
Профиль версии со ссылками на картинки |
POST |
/api/v1/themes/{id}/install |
Отметка об установке |
PUT |
/api/v1/themes/{id}/like, DELETE |
Лайк и его снятие |
POST |
/api/v1/themes/{id}/reports |
Жалоба |
GET |
/api/v1/me/themes |
Свои темы со всеми версиями и решениями |
GET |
/api/v1/assets/{sha256} |
Картинка |
Публикация — multipart/form-data: поля title, description,
changelog (у новой версии), файлы payload (JSON профиля), cover и до
шести screenshots.
Админские ручки живут под /api/admin/ и описаны в /docs.