Files
DndGamePlayer/scripts/generate-license-key-docx.py
T
2026-05-18 10:06:47 +08:00

299 lines
11 KiB
Python
Raw 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.
# -*- coding: utf-8 -*-
"""Generate TTRPG-License-Key-Instructions.docx (run: python scripts/generate-license-key-docx.py)."""
from __future__ import annotations
from pathlib import Path
from docx import Document
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.shared import Pt
ROOT = Path(__file__).resolve().parents[1]
OUT_PATHS = [
ROOT / "docs" / "TTRPG-License-Key-Instructions.docx",
Path(r"D:\TTRPG-Release\TTRPG-License-Key-Instructions.docx"),
]
def add_bullet(doc: Document, text: str, level: int = 0) -> None:
style = "List Bullet" if level == 0 else "List Bullet 2"
doc.add_paragraph(text, style=style)
def add_step(doc: Document, n: int, title: str, body: str) -> None:
p = doc.add_paragraph()
p.add_run(f"Шаг {n}. {title}. ").bold = True
p.add_run(body)
def add_code(doc: Document, text: str) -> None:
for line in text.strip().splitlines():
p = doc.add_paragraph(line)
p.style = "No Spacing"
for run in p.runs:
run.font.name = "Consolas"
run.font.size = Pt(9)
def build_document() -> Document:
doc = Document()
normal = doc.styles["Normal"]
normal.font.name = "Calibri"
normal.font.size = Pt(11)
title = doc.add_heading("TTRPG Player — выдача нового лицензионного ключа", level=0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
doc.add_paragraph(
"Инструкция для администратора: добавление нового продуктового ключа в базу "
"сервера лицензий. Пользователь вводит этот ключ в приложении; сервер выдаёт "
"подписанный лицензионный токен при активации."
)
doc.add_heading("Термины", level=1)
doc_terms = [
(
"Продуктовый ключ",
"строка вида TTRPG-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX — выдаётся покупателю, "
"хранится в data.json на сервере.",
),
(
"Лицензионный токен",
"выдаётся автоматически при активации в приложении; вручную создавать не нужно.",
),
(
"sub",
"внутренний идентификатор лицензии на сервере (отзыв, учёт устройств).",
),
]
for term, desc in doc_terms:
p = doc.add_paragraph()
p.add_run(f"{term}").bold = True
p.add_run(desc)
doc.add_heading("Где лежит сервер (продакшен)", level=1)
srv = doc.add_table(rows=6, cols=2)
srv.style = "Table Grid"
srv.rows[0].cells[0].text = "Параметр"
srv.rows[0].cells[1].text = "Значение"
server_rows = [
("Домен", "https://license.mailib.ru/"),
("VPS", "185.173.94.234"),
("Папка проекта", "/var/www/license_mailib_ru"),
("Файл ключей", "/var/www/license_mailib_ru/data.json"),
("Пользователь Linux", "dndlicense"),
]
for i, (k, v) in enumerate(server_rows, start=1):
srv.rows[i].cells[0].text = k
srv.rows[i].cells[1].text = v
doc.add_heading("Редактирование data.json (удобный редактор)", level=1)
doc.add_paragraph(
"Не используйте nano, если нужны выделение мышью и привычное копирование. "
"Рекомендуется Cursor или VS Code с расширением Remote - SSH."
)
add_step(
doc,
1,
"Настроить SSH",
"В %USERPROFILE%\\.ssh\\config добавьте хост (ключ ttrpg_updates_root — тот же, "
"что для публикации обновлений):",
)
add_code(
doc,
"""Host mailib-vps
HostName 185.173.94.234
User root
IdentityFile C:\\Users\\Administrator\\.ssh\\ttrpg_updates_root""",
)
add_step(
doc,
2,
"Подключиться",
"F1 → Remote-SSH: Connect to Host… → mailib-vps.",
)
add_step(
doc,
3,
"Открыть папку",
"File → Open Folder → /var/www/license_mailib_ru → открыть data.json.",
)
add_bullet(doc, "Альтернатива: WinSCP (SFTP) → правый клик по data.json → Edit.")
add_bullet(
doc,
"Альтернатива: scp скачать файл на ПК, править в Cursor, scp залить обратно.",
)
doc.add_heading("Выдача нового ключа", level=1)
add_step(
doc,
1,
"Резервная копия",
"На сервере (SSH или терминал в Remote SSH):",
)
add_code(
doc,
"cd /var/www/license_mailib_ru\n"
"cp data.json \"data.json.bak.$(date +%F-%H%M%S)\"",
)
add_step(
doc,
2,
"Сгенерировать запись (Node.js на сервере)",
"Выполнить в каталоге /var/www/license_mailib_ru. Скрипт добавит запись в "
"productKeys и выведет новый ключ в консоль. Срок и лимит устройств — в начале скрипта.",
)
add_code(
doc,
r"""node -e "
const fs = require('node:fs');
const crypto = require('node:crypto');
const dataPath = process.env.DND_LICENSE_DATA_PATH || './data.json';
const data = JSON.parse(fs.readFileSync(dataPath, 'utf8'));
const maxDevices = 3;
const expiresAtSec = Math.floor(new Date('2027-12-31T23:59:59Z').getTime() / 1000);
const key = 'TTRPG-' + crypto.randomUUID().toUpperCase();
const sub = 'lic_' + crypto.randomUUID().replace(/-/g, '');
data.productKeys ??= [];
data.productKeys.push({
key,
sub,
pid: 'dnd_player',
maxDevices,
expiresAtSec
});
fs.writeFileSync(dataPath, JSON.stringify(data, null, 2) + '\n');
console.log('NEW PRODUCT KEY:', key);
console.log('sub:', sub);
console.log('expiresAtSec:', expiresAtSec);
console.log('maxDevices:', maxDevices);
"
""",
)
doc.add_paragraph(
"Сохраните выведенный NEW PRODUCT KEY — его передаёте пользователю. "
"Формат ключа должен соответствовать шаблону TTRPG-… (заглавные hex-символы)."
)
add_step(
doc,
3,
"Или добавить вручную в data.json",
"В массив productKeys добавьте объект (пример):",
)
add_code(
doc,
"""{
"key": "TTRPG-12345678-1234-1234-1234-123456789ABC",
"sub": "lic_unique_id",
"pid": "dnd_player",
"maxDevices": 3,
"expiresAtSec": 1830268799
}""",
)
fields = doc.add_table(rows=6, cols=2)
fields.style = "Table Grid"
fields.rows[0].cells[0].text = "Поле"
fields.rows[0].cells[1].text = "Описание"
field_rows = [
("key", "Продуктовый ключ для пользователя (уникальный)"),
("sub", "Уникальный ID лицензии на сервере"),
("pid", "Обычно dnd_player"),
("maxDevices", "Сколько разных deviceId можно активировать"),
("expiresAtSec", "Срок действия (Unix time, секунды)"),
]
for i, (k, v) in enumerate(field_rows, start=1):
fields.rows[i].cells[0].text = k
fields.rows[i].cells[1].text = v
add_step(
doc,
4,
"Права на файл (если правили от root)",
"chown dndlicense:dndlicense /var/www/license_mailib_ru/data.json",
)
add_step(
doc,
5,
"Перезапуск сервиса",
"После изменения data.json:",
)
add_code(doc, "sudo -u dndlicense pm2 restart dnd-license")
doc.add_heading("Проверка", level=1)
add_bullet(doc, "Сервер жив: curl https://license.mailib.ru/health → {\"ok\":true}")
add_bullet(
doc,
"В приложении TTRPG Player: «Указать ключ» → вставить продуктовый ключ → активация.",
)
add_bullet(
doc,
"При ошибке unknown_product_key — ключ не в data.json или опечатка; "
"too_many_devices — превышен maxDevices.",
)
doc.add_heading("Отзыв лицензии", level=1)
doc.add_paragraph(
"Чтобы отозвать лицензию по sub, добавьте sub в массив revokedSubs в data.json "
"и перезапустите pm2. Либо POST /v1/admin/revoke с Bearer-токеном администратора "
"(токен хранится только на сервере, в эту инструкцию не включён)."
)
doc.add_heading("Локальная разработка", level=1)
add_bullet(
doc,
"Исходники сервера на ПК: D:\\Work\\my_projects\\dnd_project\\DndGamePlayerLicenseServer",
)
add_bullet(doc, "Локальный data.json: скопировать из data.example.json")
add_bullet(doc, "Запуск: npm start (нужен LICENSE_PRIVATE_KEY_PEM)")
add_bullet(doc, "Порт по умолчанию: 3847")
doc.add_heading("Частые проблемы", level=1)
problems = [
(
"Ключ не принимается в приложении",
"Проверьте формат TTRPG-… (8-4-4-4-12 hex), что запись есть в productKeys, "
"сервер перезапущен после правки data.json.",
),
(
"JSON сломан после правки",
"Восстановите из data.json.bak.*; проверьте запятые и кавычки.",
),
(
"Нет прав на запись",
"Правьте от root или chown на dndlicense; файл не должен быть только для чтения.",
),
]
for prob, fix in problems:
p = doc.add_paragraph()
p.add_run(f"{prob}. ").bold = True
p.add_run(fix)
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.add_run(
"Репозиторий сервера: git.mailib.ru/ifontosh/DndGamePlayerLicenseServer"
).italic = True
return doc
def main() -> None:
doc = build_document()
for path in OUT_PATHS:
path.parent.mkdir(parents=True, exist_ok=True)
doc.save(str(path))
print(f"Wrote {path}")
if __name__ == "__main__":
main()