# certgen — сервис генерации сертификатов и благодарственных писем Небольшой самодостаточный сервис: вы передаёте **ФИО**, **шаблон** и **дополнительный текст** — на выходе получаете документ **под номером** и **ссылку** на него (`/c/<номер>`), а также готовые **PNG** и **PDF**. Стек: чистый Python (stdlib) + [Pillow]. Внешний веб-сервер не нужен. Для HTML-шаблонов дополнительно используется headless Chromium/Chrome, если он есть в системе. ## Быстрый старт ```bash pip install -r requirements.txt # нужен только Pillow python run.py # http://127.0.0.1:8077 ``` Откройте — форма создания документа и список последних. Порт/адрес: ```bash python run.py --host 0.0.0.0 --port 8080 # или через переменные окружения: CERTGEN_HOST=0.0.0.0 CERTGEN_PORT=8080 python run.py ``` ## Что умеет - **Встроенные шаблоны**: `classic` (сертификат, A4 альбомная) и `letter` (благодарственное письмо, A4 книжная) — работают сразу, без файлов. - **Свои шаблоны-картинки**: загружаете фон (PNG/JPG) и описываете расположение текста в JSON — ФИО, заголовок, текст, дата, номер, любые свои поля. - **HTML-шаблоны**: любой HTML/CSS с плейсхолдерами `{{fio}}`, `{{title}}`, `{{body}}`, `{{date}}`, `{{number}}` и своими полями. PDF собирается Chromium. - **Нумерация**: сквозная. Первые **1000** номеров зарезервированы под особые случаи, обычная нумерация начинается с **1001** (настраивается). Необязательный префикс: `СЕРТ-001001`. - **Аккаунты и роли**: регистрация и вход по email+пароль, сессии в cookie. Обычный пользователь видит только свои документы; администратор создаёт документы и управляет пользователями. - **Ссылка на документ**: `/c/` (доступна только вошедшим — получателю или администратору). Плюс экспорт PNG/PDF. - **Публичные ссылки**: админ может включить для документа публичную ссылку `/s/<токен>` — она открывается без входа (для пересылки в письме, мессенджере). - **Пакетная генерация**: JSON-эндпоинт `/api/certificates/batch` и CLI из CSV. - **Хранение**: SQLite + файлы в `data/`. ## Веб-интерфейс - `/register`, `/login` — регистрация и вход по email и паролю. - `/` — кабинет. Администратор видит форму создания и **все** документы; обычный пользователь видит только **свои** документы (карточки с превью и скачиванием). - `/users` — управление пользователями (только админ): создание, смена роли, включение/выключение, сброс пароля, удаление. Форма создания доступна только админу. При создании указывается **email получателя** — этот пользователь увидит документ после входа под своим email. ## Пользователи и роли - **Первый зарегистрированный пользователь становится администратором.** Дальше регистрируются обычные пользователи (роль `user`). - Пароли хранятся как PBKDF2-HMAC-SHA256 (200 000 итераций, соль на пользователя). - Вход выдаёт сессию в cookie `certgen_session` (HttpOnly, SameSite=Lax, Secure за HTTPS, срок 30 дней). - **Права**: | Действие | user | admin | |---|---|---| | Регистрация/вход/смена своего пароля | ✅ | ✅ | | Просмотр своих документов | ✅ | ✅ (все) | | Создание/удаление документов, предпросмотр | ❌ | ✅ | | Шаблоны, пакетное создание | ❌ | ✅ | | Управление пользователями | ❌ | ✅ | - Ограничение попыток входа: не более 10 за 5 минут с одного IP. - Регистрацию можно закрыть: `CERTGEN_REGISTRATION=closed` (первый админ создаётся через админку или CLI). Проверить/создать админа вручную: ```bash CERTGEN_DATA=/path/to/data python3 - <<'PY' from certgen import store, auth store.init_db() if not store.get_user_by_email("admin@example.com"): store.create_user("admin@example.com", auth.hash_password("сильный-пароль"), "Админ", "admin") print("admin создан") PY ``` ## HTTP API Базовый адрес по умолчанию — `http://127.0.0.1:8077`. Все запросы, кроме `/health`, `/login`, `/register` и `POST /api/auth/*`, требуют активной сессии (cookie `certgen_session`), а часть — прав администратора. ### Аутентификация ``` POST /api/auth/register { "email": "...", "password": "...", "name": "..." } POST /api/auth/login { "email": "...", "password": "..." } POST /api/auth/logout POST /api/auth/password { "current": "...", "password": "новый" } GET /api/auth/me ``` Успешный вход/регистрация ставит cookie сессии. Первый пользователь получает роль `admin`. ### Пользователи (только админ) ``` GET /api/users POST /api/users { "email", "password", "name", "role": "user|admin" } PATCH /api/users/ { "role"?, "is_active"?, "name"?, "password"? } DELETE /api/users/ ``` ### Создать документ (только админ) ``` POST /api/certificates Content-Type: application/json { "template": "classic", // slug шаблона "fio": "Иванов Иван Иванович", // обязательно "email": "user@example.com", // email получателя: он увидит документ у себя "title": "СЕРТИФИКАТ", // необязательно (по умолчанию из шаблона) "body": "за успешное прохождение курса «...»", "date": "16 октября 2026", // необязательно, по умолчанию — сегодня "prefix": "СЕРТ", // необязательно "number": 7, // необязательно: особый номер из резерва 1..1000 "fields": { "custom": "значение" } // необязательно, свои поля {custom} } ``` Ответ (`201`): ```json { "seq": 1001, "number": "СЕРТ-001001", "template": "classic", "fio": "Иванов Иван Иванович", "recipient_email": "user@example.com", "url": "https://cert.my-dpr.ru/c/1001", "page": "https://cert.my-dpr.ru/c/1001", "png": "https://cert.my-dpr.ru/c/1001.png", "pdf": "https://cert.my-dpr.ru/c/1001.pdf" } ``` ### Остальные эндпоинты | Метод | Путь | Доступ | Описание | |--------|------|--------|----------| | GET | `/health` | все | проверка живости | | GET | `/`, `/login`, `/register`, `/users` | — | веб-страницы | | GET | `/api/templates[/]` | админ | шаблоны | | POST/DELETE | `/api/templates[...]` | админ | изменение шаблонов | | GET | `/api/certificates?limit=&offset=` | вход | user — свои, admin — все | | GET | `/api/certificates/` | вход | один документ (с проверкой владения) | | DELETE | `/api/certificates/` | админ | удалить документ и файлы | | POST | `/api/certificates/batch` | админ | пакетное создание | | POST | `/api/preview` | админ | вернуть PNG без сохранения | | POST | `/api/certificates//share` | админ | включить/обновить публичную ссылку | | DELETE | `/api/certificates//share` | админ | отключить публичную ссылку | | GET | `/c/` и `.png` / `.pdf` | вход | доступ: получатель или админ | | GET | `/s/<токен>` и `.png` / `.pdf` | **публично** | доступ по публичной ссылке | ### Публичные ссылки Администратор включает публичную ссылку в кабинете (кнопка «Сделать публичной») или через API: ```bash curl -X POST https://cert.my-dpr.ru/api/certificates/1001/share -b cookies.txt # -> { "public_url": "https://cert.my-dpr.ru/s/", ... } # отключить: curl -X DELETE https://cert.my-dpr.ru/api/certificates/1001/share -b cookies.txt ``` Публичная ссылка не требует входа и отдаёт страницу, PNG и PDF. Токен случайный (≈43 символа); при включении заново выдается новый токен, старый перестаёт работать. Чтобы закрыть доступ — отключите публичную ссылку. ### Пакетное создание (только админ) ``` POST /api/certificates/batch { "template": "letter", "body": "за участие", "names": ["Первый Участник", "Второй Участник"] } ``` В `names` можно передавать строки (ФИО) или объекты с полями конкретного документа. ## Нумерация - Обычные документы нумеруются последовательно, **начиная с 1001**: `001001`, `001002`, … (ширину ведущих нулей задаёт `CERTGEN_PAD`, по умолчанию 6). - Диапазон **1…1000 зарезервирован под особые случаи**. Чтобы выдать конкретный номер оттуда, передайте `number` (в API/CLI или поле «Особый номер» в форме). Повторное использование занятого номера вернёт ошибку. - Номер отображается как `number` (с префиксом, если задан), а ссылка строится по внутреннему `seq`; работает и открытие по самому номеру: `/c/<номер>`. - Стартовый номер меняется переменной `CERTGEN_START` (например, `CERTGEN_START=1`, чтобы вообще не резервировать диапазон). ```bash # особый случай под номером 7 curl -X POST localhost:8077/api/certificates -H 'Content-Type: application/json' \ -d '{"template":"classic","fio":"Иванов И.И.","number":7}' ``` ## Шаблоны ### Встроенные - `classic` — «СЕРТИФИКАТ», A4 альбомная (1754×1240). - `letter` — «БЛАГОДАРСТВЕННОЕ ПИСЬМО», A4 книжная (1240×1754). ### Шаблон по готовому дизайну (пример «Созидатели») Если у вас есть готовый макет (PNG/JPG), где заголовок, текст и подпись уже нарисованы, а пустое место оставлено под ФИО — такой шаблон делается за минуту: фон = картинка, поверх накладывается только имя. ```bash python examples/make_sozidateli_template.py "Сертификат Созидатели.png" ``` Скрипт увеличивает фон ×2, кладёт его в `data/templates/custom//background.png` и регистрирует image-шаблон `sozidateli` (ФИО по центру, шрифт NotoSans-Bold). Готовый макет и логику можно взять за образец для других дизайнов. > В проект вложен шрифт `certgen/assets/fonts/NotoSans-Bold.ttf` (лицензия OFL, > см. `LICENSE-NotoSans.txt`) — чтобы рендер выглядел одинаково на любой машине, > даже если системного Noto Sans нет. ### Свой шаблон-картинка ``` POST /api/templates { "slug": "dark", "title": "Тёмный сертификат", "kind": "image", "background_b64": "", "page": { "width": 1600, "height": 1131 }, "defaults": { "title": "СЕРТИФИКАТ", "body": "подтверждение участия" }, "layout": [ { "key": "title", "x": 0.5, "y": 0.2, "size": 90, "font": "sans-bold", "color": "#e0bd57", "align": "center", "uppercase": true, "letter_spacing": 8 }, { "key": "fio", "x": 0.5, "y": 0.45, "size": 96, "font": "serif-bold", "color": "#ffffff", "align": "center", "max_width": 0.85 }, { "text": "выдан 2026", "x": 0.5, "y": 0.9, "size": 26, "color": "#888", "align": "center" } ] } ``` Поля элемента `layout`: - `key` — имя подставляемого значения (`fio`, `title`, `body`, `date`, `number` или ключ из `fields`); вместо `key` можно задать `text` — статичный текст. - `x`, `y` — координаты: доля от 0 до 1 (от ширины/высоты фона) либо пиксели. - `size` — размер шрифта в px относительно `page.height`. - `font` — `serif`, `serif-bold`, `serif-italic`, `sans`, `sans-bold`, `sans-italic`, `mono` или имя/путь к TTF. - `color` — `#rrggbb`; `align` — `left` / `center` / `right`. - `max_width` — доля ширины для переноса строк (необязательно). - `uppercase`, `letter_spacing`, `line_spacing` — оформление (необязательно). В любом тексте (и в `text`, и в `body`) можно писать `{fio}`, `{{fio}}`, `{custom}` — значения подставятся. ### HTML-шаблон ``` POST /api/templates { "slug": "htmlcert", "title": "HTML сертификат", "kind": "html", "html": "... {{fio}} ... {{number}} ...", "page": { "width": 1600, "height": 1131 }, "defaults": { "title": "СЕРТИФИКАТ", "body": "подтверждает участие" } } ``` Готовый пример — `examples/sample.html`. Задайте размер страницы через `@page` (для PDF) и заполняйте кадр через `100vw/100vh` (для PNG-скриншота, размер которого берётся из `page.width` × `page.height`). > HTML-шаблоны требуют Chromium/Chrome. Если его нет, они вернут понятную ошибку, > а картинки-шаблоны продолжат работать. ## CLI (пакетно, без сервера) ```bash # один документ python -m certgen.gen --template classic \ --fio "Иванов Иван Иванович" --body "за успешное прохождение курса" --outdir out # из CSV (см. examples/names.csv), с копированием файлов в out/ python -m certgen.gen --template letter --csv examples/names.csv --outdir out # список через ';' python -m certgen.gen --template classic --names "Иванов И.И.;Петров П.П." ``` Документы сохраняются в общую базу, поэтому нумерация общая с сервером, и позже их можно открыть по ссылке `/c/<номер>`. CLI работает с локальной базой напрямую (в обход веб-авторизации) — для этого нужен доступ к файлу `data/certs.db`. Чтобы привязать документ к получателю, передайте `--email user@example.com`. ## Данные ``` data/ certs.db # SQLite: документы и шаблоны output/000001.png # сгенерированные файлы output/000001.pdf templates/ # фоны шаблонов tmp/ # временные файлы ``` Каталог данных можно вынести: `CERTGEN_DATA=/path/to/data`. ## Развёртывание - Сервис рассчитан на запуск за reverse-proxy (nginx/caddy). Публичный адрес для ссылок задайте явно: `CERTGEN_BASE_URL=https://certs.example.org`. - Если проксируете, включите передачу заголовков `Host` и `X-Forwarded-Proto`. - Для доступа извне: `python run.py --host 0.0.0.0` и, желательно, авторизация на прокси. - Запуск как systemd-юнита: ```ini [Unit] Description=certgen After=network.target [Service] WorkingDirectory=/opt/certgen ExecStart=/usr/bin/python3 run.py --host 127.0.0.1 --port 8077 Environment=CERTGEN_BASE_URL=https://certs.example.org Restart=on-failure [Install] WantedBy=multi-user.target ``` ## Переменные окружения | Переменная | По умолчанию | Значение | |-----------|--------------|----------| | `CERTGEN_HOST` | `127.0.0.1` | адрес прослушивания | | `CERTGEN_PORT` | `8077` | порт | | `CERTGEN_DATA` | `./data` | каталог данных | | `CERTGEN_BASE_URL` | — | публичный базовый URL для ссылок | | `CERTGEN_PDF_DPI` | `150` | DPI при экспорте картинки в PDF | | `CERTGEN_START` | `1001` | первый номер обычной нумерации (1..START-1 — резерв) | | `CERTGEN_PAD` | `6` | ширина номера с ведущими нулями | | `CERTGEN_REGISTRATION` | `open` | `open` — свободная регистрация; `closed` — только админ | | `CERTGEN_ADMIN_EMAIL` | — | email, получающий роль админа при регистрации | | `CERTGEN_SESSION_DAYS` | `30` | срок жизни сессии в днях | ## Лицензия MIT (используйте и меняйте свободно). [Pillow]: https://python-pillow.org/