Files

375 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/