Initial commit: certgen — сервис генерации сертификатов
This commit is contained in:
commit
49bac43e16
23 files changed
+3046
No files matched your search
@@ -0,0 +1,374 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
Откройте <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/<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). Проверить/создать админа вручную:
|
||||
|
||||
```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/<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`):
|
||||
|
||||
```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[/<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:
|
||||
|
||||
```bash
|
||||
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`,
|
||||
чтобы вообще не резервировать диапазон).
|
||||
|
||||
```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/<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 (пакетно, без сервера)
|
||||
|
||||
```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/
|
||||
Reference in new issue
Block a user