feat: e-signature acceptance — append-only journal with SHA-256 doc hash (63-FZ)

This commit is contained in:
wasrusgen 2026-05-30 15:20:27 +03:00
parent d378d47421
commit 1b333debd9

View File

@ -117,6 +117,19 @@ def init_db():
created_at TEXT, created_at TEXT,
FOREIGN KEY(project_id) REFERENCES projects(id) FOREIGN KEY(project_id) REFERENCES projects(id)
); );
CREATE TABLE IF NOT EXISTS acceptances (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
doc TEXT NOT NULL, -- 'offer' | 'pep' | 'pdn'
doc_version TEXT NOT NULL,
doc_hash TEXT NOT NULL, -- SHA-256 текста документа (что подписано)
identifier TEXT, -- телефон/email подписанта
code TEXT, -- код подтверждения
ip TEXT,
user_agent TEXT,
payment_id TEXT, -- акцепт оплатой
accepted_at TEXT NOT NULL -- append-only, не редактируется
);
""") """)
con.commit() con.commit()
con.close() con.close()
@ -631,6 +644,56 @@ def update_crm():
save_artifact(proj["id"], "crm", crm) save_artifact(proj["id"], "crm", crm)
return jsonify({"ok": True, "crm": crm}) return jsonify({"ok": True, "crm": crm})
import hashlib
# Версии и тексты юридических документов (хеш фиксируется при акцепте)
LEGAL_DOCS = {"offer": "1.0", "pep": "1.0", "pdn": "1.0"}
def _doc_hash(doc):
"""SHA-256 текста документа — для доказательства что подписано."""
path = os.path.join(BASE, "legal", {"offer": "dogovor_oferta.md", "pep": "soglashenie_pep.md", "pdn": "politika_pdn.md"}.get(doc, ""))
try:
return hashlib.sha256(open(path, "rb").read()).hexdigest()
except Exception:
return ""
@app.route("/api/accept", methods=["POST"])
def accept_documents():
"""Фиксация акцепта (ПЭП) в append-only журнал с хешем документов."""
data = request.get_json(force=True) or {}
proj = get_project(data.get("token"))
if not proj:
return jsonify({"error": "project not found"}), 404
docs = data.get("docs", ["offer", "pep"]) # какие документы акцептованы
identifier = data.get("identifier", "") # телефон/email
code = data.get("code", "")
payment_id = data.get("payment_id", "")
ip = request.headers.get("X-Real-IP") or request.headers.get("X-Forwarded-For", request.remote_addr or "")
ua = request.headers.get("User-Agent", "")[:300]
con = db()
recorded = []
for doc in docs:
if doc not in LEGAL_DOCS:
continue
h = _doc_hash(doc)
con.execute(
"INSERT INTO acceptances (project_id, doc, doc_version, doc_hash, identifier, code, ip, user_agent, payment_id, accepted_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
(proj["id"], doc, LEGAL_DOCS[doc], h, identifier, code, ip, ua, payment_id, now())
)
recorded.append({"doc": doc, "version": LEGAL_DOCS[doc], "hash": h[:16] + "..."})
con.commit()
return jsonify({"ok": True, "accepted": recorded, "at": now()})
@app.route("/api/acceptances/<token>")
def get_acceptances(token):
"""Выписка из журнала акцептов (доказательная база)."""
proj = get_project(token)
if not proj:
return jsonify({"error": "not found"}), 404
rows = db().execute(
"SELECT doc, doc_version, doc_hash, identifier, code, ip, payment_id, accepted_at FROM acceptances WHERE project_id=? ORDER BY id", (proj["id"],)
).fetchall()
return jsonify({"acceptances": [dict(r) for r in rows]})
@app.route("/api/payment/create", methods=["POST"]) @app.route("/api/payment/create", methods=["POST"])
def payment_create(): def payment_create():
"""Создаёт платёж. method: card | sbp | cash.""" """Создаёт платёж. method: card | sbp | cash."""