No description
  • Python 75.2%
  • TypeScript 19%
  • CSS 4.6%
  • Dockerfile 0.5%
  • HTML 0.4%
  • Other 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
xfx1337 0fc5abc239 fix: не раздавать настоящий student_id и ограничить злоупотребления
student_id — ключ от аккаунта: зная его, можно привязать своё устройство и
действовать от чужого имени (и на бирже пар тоже). API отдавал его всем — в
авторах тем и в списках участников. Теперь наружу уходит случайный
public_id; поле в списках участников осталось student_id, чтобы выпущенное
расширение работало без обновления.

Плюс то, что сервер не может проверить, настоящий ли студент:
- student_id только в формате UUID;
- лимиты по IP на новых студентов, публикации, комнаты, неверные коды,
  жалобы и входы в админку;
- квота места на студента и запас свободного места на диске.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 03:38:48 +03:00
admin feat: мастерская тем — комнаты, версии, модерация, админка 2026-10-01 18:58:33 +03:00
alembic fix: не раздавать настоящий student_id и ограничить злоупотребления 2026-10-03 03:38:48 +03:00
app fix: не раздавать настоящий student_id и ограничить злоупотребления 2026-10-03 03:38:48 +03:00
tests fix: не раздавать настоящий student_id и ограничить злоупотребления 2026-10-03 03:38:48 +03:00
.dockerignore feat: мастерская тем — комнаты, версии, модерация, админка 2026-10-01 18:58:33 +03:00
.env.example feat: мастерская тем — комнаты, версии, модерация, админка 2026-10-01 18:58:33 +03:00
.gitignore feat: мастерская тем — комнаты, версии, модерация, админка 2026-10-01 18:58:33 +03:00
alembic.ini feat: мастерская тем — комнаты, версии, модерация, админка 2026-10-01 18:58:33 +03:00
docker-compose.yml feat: мастерская тем — комнаты, версии, модерация, админка 2026-10-01 18:58:33 +03:00
Dockerfile feat: мастерская тем — комнаты, версии, модерация, админка 2026-10-01 18:58:33 +03:00
pyproject.toml chore: репозиторий называется lms-workshop 2026-10-01 22:44:21 +03:00
README.md fix: не раздавать настоящий student_id и ограничить злоупотребления 2026-10-03 03:38:48 +03:00
requirements-dev.txt feat: мастерская тем — комнаты, версии, модерация, админка 2026-10-01 18:58:33 +03:00
requirements.txt feat: мастерская тем — комнаты, версии, модерация, админка 2026-10-01 18:58:33 +03:00

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.