Files

19 KiB
Raw Permalink Blame History

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 (используйте и меняйте свободно).