Техническая документация — Платформа визиток МФТИ ВШПИ
Содержание
2. Архитектура
6. База данных
9. Безопасность
11. Миграции БД
1. Обзор системы
Платформа визиток студентов МФТИ ВШПИ — веб-приложение на Python/Flask, позволяющее студентам Высшей школы программной инженерии МФТИ публиковать персональные HTML-визитки.
Ключевые возможности:
- Регистрация с подтверждением email (только адреса
@phystech.edu) - Загрузка HTML-визиток с автоматической валидацией структуры
- Модерация через административную панель
- Автоматическая генерация URL-эндпоинта
/cards/<username>при одобрении - Система лайков/дизлайков (AJAX, без перезагрузки страницы)
- Поиск и сортировка визиток
- Мягкое удаление (файлы сохраняются для возможного восстановления)
- Управление правами администраторов
Технологический стек:
- Backend: Python 3.10+, Flask 3.0
- ORM: SQLAlchemy 2.0 + SQLite
- Аутентификация: Flask-Login
- Формы и CSRF: Flask-WTF
- HTML-санитизация: bleach + tinycss2
- Парсинг HTML: BeautifulSoup4
- Хэширование паролей: bcrypt
- Email: smtplib (встроенный, без внешних зависимостей)
2. Архитектура
Приложение построено по паттерну Application Factory с разделением на три Blueprint-а.
┌─────────────────────────────────────────────────────────┐
│ Flask Application │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ auth │ │ cards │ │ admin │ │
│ │ Blueprint │ │ Blueprint │ │ Blueprint │ │
│ │ │ │ │ │ │ │
│ │ /register │ │ / │ │ /admin/ │ │
│ │ /login │ │ /submit │ │ /admin/users │ │
│ │ /logout │ │ /cards/<s> │ │ /admin/submiss. │ │
│ │ /verify-* │ │ /vote │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ SQLAlchemy ORM │ │
│ │ User | Submission | BusinessCard | Vote │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ sanitizer │ │ validators │ │ mailer │ │
│ │ (bleach) │ │ (bs4) │ │ (smtplib) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
│
┌───────────┴───────────┐
│ SQLite DB │
└───────────────────────┘
3. Структура проекта
mipt_cards/
├── app/
│ ├── __init__.py # Фабрика create_app(), загрузка .env
│ ├── models.py # ORM-модели: User, Submission, BusinessCard, Vote
│ ├── sanitizer.py # HTML-санитайзер на базе bleach
│ ├── validators.py # Проверка структуры HTML-визитки
│ ├── mailer.py # Отправка email через SMTP
│ ├── auth/
│ │ ├── __init__.py # Blueprint регистрации
│ │ └── routes.py # /register, /login, /logout, /verify-email
│ ├── cards/
│ │ ├── __init__.py # Blueprint визиток
│ │ └── routes.py # /, /submit, /cards/<slug>, /vote, /delete
│ ├── admin/
│ │ ├── __init__.py # Blueprint администратора
│ │ └── routes.py # /admin/, /admin/users, /admin/submissions/
│ └── templates/
│ ├── base.html # Базовый шаблон с навигацией
│ ├── index.html # Главная страница
│ ├── auth/
│ │ ├── login.html
│ │ ├── register.html
│ │ └── verify_email.html
│ ├── cards/
│ │ ├── submit.html # Форма загрузки визитки
│ │ └── view.html # Просмотр визитки (fullscreen iframe)
│ └── admin/
│ ├── index.html # Список заявок на модерацию
│ ├── submission.html # Просмотр и обработка заявки
│ └── users.html # Управление пользователями
├── static/
│ ├── css/
│ │ └── main.css # Стили (VSHPI Neon Glass дизайн)
│ └── js/
│ └── vote.js # AJAX-логика голосования
├── uploads/
│ ├── pending/ # Загруженные файлы до модерации
│ └── approved/ # Санитизированные одобренные файлы
├── config.py # Конфигурация приложения
├── run.py # Точка входа
├── create_admin.py # Скрипт создания первого администратора
├── migrate_add_deleted_at.py # Миграция: поле deleted_at
├── migrate_admin_fields.py # Миграция: поля deleted_by_admin, admin_message_until
├── requirements.txt
├── .env # Переменные окружения (не коммитить!)
└── cards.db # SQLite база данных
4. Установка и запуск
Требования
- Python 3.10 или новее
- pip
Шаги установки
# 1. Перейти в директорию проекта
cd mipt_cards
# 2. Создать виртуальное окружение (рекомендуется)
python -m venv .venv
.venv\Scripts\activate # Windows
source .venv/bin/activate # Linux/macOS
# 3. Установить зависимости
pip install -r requirements.txt
# 4. Создать файл .env с настройками (см. раздел 5)
# 5. Применить миграции (если обновляете существующую БД)
python migrate_add_deleted_at.py
python migrate_admin_fields.py
# 6. Создать первого администратора
python create_admin.py
# 7. Запустить сервер
python run.py
Приложение будет доступно по адресу: http://localhost:5000
5. Переменные окружения
Все переменные задаются в файле .env в корне директории mipt_cards/.
| Переменная | Обязательная | По умолчанию | Описание |
|---|---|---|---|
SECRET_KEY | Да (в prod) | change-me-in-production-please | Ключ подписи сессий и CSRF-токенов |
DATABASE_URL | Нет | sqlite:///cards.db | URI базы данных |
FLASK_ENV | Нет | development | Режим запуска (development / production) |
MAIL_SERVER | Да | smtp.gmail.com | SMTP-сервер |
MAIL_PORT | Да | 587 | Порт SMTP |
MAIL_USE_TLS | Нет | true | Использовать STARTTLS (порт 587) |
MAIL_USE_SSL | Нет | false | Использовать SSL (порт 465, mail.ru) |
MAIL_USERNAME | Да | — | Логин SMTP-аккаунта |
MAIL_PASSWORD | Да | — | Пароль SMTP-аккаунта |
MAIL_FROM | Нет | = MAIL_USERNAME | Адрес отправителя |
EMAIL_CODE_TTL | Нет | 600 | Время жизни кода подтверждения (секунды) |
Пример .env для mail.ru
SECRET_KEY=your-very-secret-key-here
MAIL_SERVER=smtp.mail.ru
MAIL_PORT=465
MAIL_USE_TLS=False
MAIL_USE_SSL=True
MAIL_USERNAME=your@mail.ru
MAIL_PASSWORD=your_app_password
MAIL_FROM=your@mail.ru
Пример .env для Gmail
SECRET_KEY=your-very-secret-key-here
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USE_TLS=True
MAIL_USE_SSL=False
MAIL_USERNAME=your@gmail.com
MAIL_PASSWORD=your_app_password
MAIL_FROM=your@gmail.com
> Важно: Для Gmail используйте App Password (пароль приложения), а не основной пароль аккаунта.
6. База данных
Схема
Таблица users
| Поле | Тип | Описание |
|---|---|---|
id | INTEGER PK | Идентификатор |
username | VARCHAR(64) UNIQUE | Имя пользователя (латиница, цифры, _, -) |
email | VARCHAR(120) UNIQUE | Email в формате *@phystech.edu |
password_hash | VARCHAR(128) | bcrypt-хэш пароля |
is_admin | BOOLEAN | Флаг администратора |
created_at | DATETIME | Дата регистрации (UTC) |
Таблица submissions
| Поле | Тип | Описание |
|---|---|---|
id | INTEGER PK | Идентификатор |
user_id | INTEGER FK→users | Автор заявки |
filename | VARCHAR(256) | UUID-имя файла на диске |
original_filename | VARCHAR(256) | Оригинальное имя файла |
status | VARCHAR(16) | pending / approved / rejected |
rejection_reason | TEXT | Причина отклонения (если есть) |
submitted_at | DATETIME | Дата подачи (UTC) |
reviewed_at | DATETIME | Дата рассмотрения (UTC) |
reviewed_by | INTEGER FK→users | Администратор, рассмотревший заявку |
Таблица business_cards
| Поле | Тип | Описание |
|---|---|---|
id | INTEGER PK | Идентификатор |
user_id | INTEGER FK→users UNIQUE | Владелец (один пользователь — одна карточка) |
submission_id | INTEGER FK→submissions | Заявка, из которой создана карточка |
slug | VARCHAR(64) UNIQUE | URL-идентификатор (= username) |
filename | VARCHAR(256) | Путь к санитизированному HTML-файлу |
approved_at | DATETIME | Дата одобрения (UTC) |
likes_count | INTEGER | Денормализованный счётчик лайков |
dislikes_count | INTEGER | Денормализованный счётчик дизлайков |
deleted_at | DATETIME | Дата мягкого удаления (NULL = активна) |
deleted_by_admin | BOOLEAN | Удалена администратором |
admin_message_until | DATETIME | До какого времени показывать сообщение об удалении |
Таблица votes
| Поле | Тип | Описание |
|---|---|---|
id | INTEGER PK | Идентификатор |
voter_id | INTEGER FK→users | Голосующий |
card_id | INTEGER FK→business_cards | Визитка |
vote_type | VARCHAR(8) | like / dislike |
created_at | DATETIME | Дата голоса (UTC) |
Ограничение: UNIQUE(voter_id, card_id) — один пользователь, один голос на визитку.
ER-диаграмма
users ──────────────── submissions
│ 1 │ *
│ │
│ 1 │ 1
└──── business_cards ───┘
│ 1
│
│ *
votes
│ *
│
│ 1
users
7. API и маршруты
Публичные маршруты
| Метод | URL | Описание |
|---|---|---|
| GET | / | Главная страница со списком визиток |
| GET | /cards/<slug> | Просмотр визитки |
| GET | /cards/<slug>/raw | Сырой HTML визитки (для iframe) |
| GET | /login | Форма входа |
| POST | /login | Обработка входа |
| GET | /register | Форма регистрации |
| POST | /register | Обработка регистрации, отправка кода |
| GET | /verify-email | Форма ввода кода подтверждения |
| POST | /verify-email | Проверка кода, создание аккаунта |
| POST | /verify-email/resend | Повторная отправка кода |
Маршруты для авторизованных пользователей
| Метод | URL | Описание |
|---|---|---|
| GET | /logout | Выход из системы |
| GET | /submit | Форма загрузки/обновления визитки |
| POST | /submit | Загрузка HTML-файла |
| POST | /cards/<slug>/vote | Лайк/дизлайк (AJAX, JSON) |
| POST | /cards/<slug>/delete | Мягкое удаление визитки |
Маршруты администратора
| Метод | URL | Описание |
|---|---|---|
| GET | /admin/ | Список заявок на модерацию |
| GET | /admin/submissions/<id> | Просмотр заявки с предпросмотром |
| POST | /admin/submissions/<id>/approve | Одобрить заявку |
| POST | /admin/submissions/<id>/reject | Отклонить заявку с причиной |
| GET | /admin/users | Список всех пользователей |
| POST | /admin/users/<id>/toggle-admin | Выдать/отозвать права администратора |
AJAX API: голосование
POST /cards/<slug>/vote
Request (JSON):
{ "vote_type": "like" }
Response (JSON):
{
"action": "added",
"vote_type": "like",
"likes": 5,
"dislikes": 1,
"user_vote": "like"
}
Возможные значения action: added, removed, changed.
Возможные значения vote_type: like, dislike.
user_vote: null означает что голос отменён.
HTTP-коды ответов: 200 OK, 401 Unauthorized, 403 Forbidden, 404 Not Found.
8. Модули приложения
app/__init__.py — фабрика приложения
Функция create_app(config_name):
1. Загружает .env файл до импорта конфига
2. Создаёт Flask-приложение
3. Перечитывает email-настройки из os.environ (обход кэширования)
4. Инициализирует расширения: SQLAlchemy, Flask-Login, CSRFProtect
5. Регистрирует blueprints: auth, cards, admin
6. Создаёт таблицы БД через db.create_all()
7. Устанавливает заголовки безопасности через after_request
app/models.py — модели данных
User— пользователь. Методыset_password()иcheck_password()используют bcrypt.Submission— заявка на публикацию визитки. Статусы:pending,approved,rejected.BusinessCard— опубликованная визитка. Поддерживает мягкое удаление черезdeleted_at.Vote— голос пользователя. Уникальное ограничение(voter_id, card_id).
app/sanitizer.py — HTML-санитайзер
Функция sanitize_html(html_content):
1. Удаляет теги <script>, <iframe>, <object>, <embed>, <noscript>, <base> вместе с содержимым
2. Удаляет атрибуты обработчиков событий (on*)
3. Удаляет javascript: в href/src
4. Применяет bleach с белым списком тегов и атрибутов
5. Вставляет <base target="_blank"> для открытия ссылок в новой вкладке
app/validators.py — валидатор визитки
Функция validate_card_html(html_content) → (is_valid: bool, errors: list[str]):
Проверяет:
- Наличие
<title>с ФИО - Наличие
<img>с атрибутомalt - Раздел «О себе» (≥ 50 символов)
- Раздел «Учёба» с упоминанием МФТИ/ВШПИ
- Раздел «Навыки» (≥ 3 пункта)
- Контактная информация (email или ссылка на соцсеть)
- Отсутствие запрещённых тегов (
<script>,<iframe>,<object>,<embed>) - Отсутствие атрибутов обработчиков событий
app/mailer.py — отправка email
Функция send_verification_email(to_email, username, code) → bool:
- Поддерживает SSL (
MAIL_USE_SSL=True, порт 465) и STARTTLS (MAIL_USE_TLS=True, порт 587) - Отправляет HTML + plain text письмо с 6-значным кодом
- В dev-режиме (без настроенного SMTP) выводит код в лог
app/auth/routes.py — авторизация
Процесс регистрации с подтверждением email:
1. Пользователь заполняет форму → данные сохраняются в Flask-сессии
2. Пароль хэшируется сразу (не хранится в открытом виде)
3. Генерируется 6-значный цифровой код, отправляется на email
4. Пользователь вводит код на /verify-email
5. Защита от брутфорса: после 5 неверных попыток сессия сбрасывается
6. Код действителен 10 минут (настраивается через EMAIL_CODE_TTL)
9. Безопасность
Аутентификация и авторизация
- Пароли хэшируются через bcrypt с cost factor 12
- Сессии подписываются через
SECRET_KEY - CSRF-защита на всех формах через Flask-WTF
- Декоратор
@admin_requiredзащищает административные маршруты (HTTP 403 для не-администраторов)
Изоляция контента визиток
- Загруженные файлы хранятся в
uploads/внеstatic/— недоступны по прямому URL - Визитки отображаются через
<iframe sandbox="allow-same-origin">— скрипты заблокированы - При одобрении HTML санитизируется через bleach — удаляются все опасные теги и атрибуты
- Ссылки в визитках открываются в новой вкладке через
<base target="_blank">
HTTP-заголовки безопасности
X-Content-Type-Options: nosniff
X-Frame-Options: DENY (кроме /cards/<slug>/raw)
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self'; img-src 'self' data: https:;
Валидация загружаемых файлов
- Проверка расширения файла (только
.html) - Проверка размера (максимум 512 КБ)
- Структурная валидация HTML через BeautifulSoup4
- Санитизация перед публикацией
10. Требования к визитке
HTML-файл визитки студента МФТИ ВШПИ должен соответствовать следующим требованиям:
| # | Требование | Проверка |
|---|---|---|
| 1 | Валидный HTML5-документ | Парсинг через BeautifulSoup4 |
| 2 | Тег <title> с ФИО студента | Наличие непустого <title> |
| 3 | Фотография <img> с атрибутом alt | Наличие <img alt="..."> |
| 4 | Раздел «О себе» (≥ 50 символов) | Заголовок содержит «о себе» / «обо мне» |
| 5 | Раздел «Учёба» с курсом, группой, направлением МФТИ ВШПИ | Заголовок содержит «учёба» / «образование» |
| 6 | Раздел «Навыки» (≥ 3 технических навыка) | Заголовок содержит «навыки» / «skills» |
| 7 | Контактная информация (email или соцсеть) | Regex email или ссылка на VK/Telegram/GitHub/LinkedIn |
| 8 | Без <script> и обработчиков событий (on*) | Regex-поиск |
| 9 | Без <iframe>, <object>, <embed> | Поиск тегов |
Ограничения файла: только .html, максимум 512 КБ.
11. Миграции БД
При обновлении существующей базы данных необходимо применить миграции:
# Добавляет поле deleted_at в business_cards
python migrate_add_deleted_at.py
# Добавляет поля deleted_by_admin и admin_message_until в business_cards
python migrate_admin_fields.py
Для новых установок миграции не нужны — db.create_all() создаёт все таблицы автоматически при первом запуске.
Лицензия
MIT License — свободное использование, модификация и распространение.