19 KiB
certgen — сервис генерации сертификатов и благодарственных писем
Небольшой самодостаточный сервис: вы передаёте ФИО, шаблон и дополнительный
текст — на выходе получаете документ под номером и ссылку на него
(/c/<номер>), а также готовые PNG и PDF.
Стек: чистый Python (stdlib) + Pillow. Внешний веб-сервер не нужен. Для HTML-шаблонов дополнительно используется headless Chromium/Chrome, если он есть в системе.
Быстрый старт
pip install -r requirements.txt # нужен только Pillow
python run.py # http://127.0.0.1:8077
Откройте http://127.0.0.1:8077 — форма создания документа и список последних.
Порт/адрес:
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/<seq>(доступна только вошедшим — получателю или администратору). Плюс экспорт 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). Проверить/создать админа вручную:
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/<id> { "role"?, "is_active"?, "name"?, "password"? }
DELETE /api/users/<id>
Создать документ (только админ)
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):
{
"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[/<slug>] |
админ | шаблоны |
| POST/DELETE | /api/templates[...] |
админ | изменение шаблонов |
| GET | /api/certificates?limit=&offset= |
вход | user — свои, admin — все |
| GET | /api/certificates/<seq|номер> |
вход | один документ (с проверкой владения) |
| DELETE | /api/certificates/<seq|номер> |
админ | удалить документ и файлы |
| POST | /api/certificates/batch |
админ | пакетное создание |
| POST | /api/preview |
админ | вернуть PNG без сохранения |
| POST | /api/certificates/<seq>/share |
админ | включить/обновить публичную ссылку |
| DELETE | /api/certificates/<seq>/share |
админ | отключить публичную ссылку |
| GET | /c/<seq|номер> и .png / .pdf |
вход | доступ: получатель или админ |
| GET | /s/<токен> и .png / .pdf |
публично | доступ по публичной ссылке |
Публичные ссылки
Администратор включает публичную ссылку в кабинете (кнопка «Сделать публичной») или через API:
curl -X POST https://cert.my-dpr.ru/api/certificates/1001/share -b cookies.txt
# -> { "public_url": "https://cert.my-dpr.ru/s/<token>", ... }
# отключить:
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, чтобы вообще не резервировать диапазон).
# особый случай под номером 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), где заголовок, текст и подпись уже нарисованы, а пустое место оставлено под ФИО — такой шаблон делается за минуту: фон = картинка, поверх накладывается только имя.
python examples/make_sozidateli_template.py "Сертификат Созидатели.png"
Скрипт увеличивает фон ×2, кладёт его в data/templates/custom/<slug>/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": "<PNG/JPG в base64>",
"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": "<html>... {{fio}} ... {{number}} ...</html>",
"page": { "width": 1600, "height": 1131 },
"defaults": { "title": "СЕРТИФИКАТ", "body": "подтверждает участие" }
}
Готовый пример — examples/sample.html. Задайте размер страницы через @page
(для PDF) и заполняйте кадр через 100vw/100vh (для PNG-скриншота, размер которого
берётся из page.width × page.height).
HTML-шаблоны требуют Chromium/Chrome. Если его нет, они вернут понятную ошибку, а картинки-шаблоны продолжат работать.
CLI (пакетно, без сервера)
# один документ
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-юнита:
[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 (используйте и меняйте свободно).