Files
site_aegisone/py_service/app/documents_router.py
T
angel 72b6879f4b v1.7.0: refactor max_bot to flat structure, add VCF+UserModel+NLP history context, portal pages and proxy fixes
- Refactored max_bot from nested packages to flat module structure
- Q2: Extended BotUser model (patronymic, email, org, address, vcf_raw, contact_hash, phone_verified, email_verified, last_interaction, total_conversations, total_tickets)
- Q2: VCF parser (FN, N, TEL, EMAIL, ORG, ADR), upsert on re-contact, NLP history context (_get_user_history_context -> YandexGPT)
- Q1: Broadcast preview modal with 10s confirmation timer
- Q3: CSS var(--white)->var(--bg-card), var(--text)->var(--text-primary)
- Q4: bot_settings showNotification(), editable max_bot_id
- Q5: Webhook secret passthrough via X-Max-Bot-Api-Secret
- Masking sensitive keys, dialog_cleared handler, migrate via _add_column_if_not_exists()
- Rate limit (asyncio.sleep 0.5 per 10), dead code removed, conv.intent context in contact.py
- Portal pages: bot_consent, bot_kb (edit), bot_settings, bot_test, bot_tickets, portal_settings
- Tests: 21/21 passing, added test_yandex_gpt.py, test_email_sender.py
- Deploy: deploy_full.sh, schema.sql, seed_knowledge_base.sql
2026-05-29 02:30:30 +03:00

292 lines
9.9 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.
import os
import re
import markdown
from pathlib import Path
from fastapi import APIRouter, Request, Depends, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from datetime import datetime
from app.config import get_settings
from app.services.docs_permissions import load_permissions, save_permissions, discover_docs, can_access_doc, get_allowed_docs
from app.dependencies import get_current_user, require_role
settings = get_settings()
router = APIRouter(prefix="/service/documents", tags=["documents"])
from app.templating import templates
def now_str():
return datetime.now().strftime("%d.%m.%Y %H:%M")
def _doc_ctx(request, user, **kw):
original_user = request.session.get("original_user")
available_roles = {"owner": ["owner", "engineer", "technician"], "engineer": ["engineer", "technician"]}
c = {"request": request, "user": user, "now": now_str(),
"role_permissions": getattr(request.state, 'role_permissions', {}),
"role_override_active": original_user is not None,
"original_role": original_user["role"] if original_user else None,
"available_roles": available_roles.get(user["role"], [user["role"]]) if user else []}
c.update(kw)
return c
def transliterate_slug(text: str) -> str:
translit_map = {
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', 'ж': 'zh',
'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o',
'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'kh', 'ц': 'ts',
'ч': 'ch', 'ш': 'sh', 'щ': 'shch', 'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu',
'я': 'ya', ' ': '_',
}
result = ''.join(translit_map.get(c.lower(), c) for c in text)
result = re.sub(r'[^a-zA-Z0-9_\-]', '', result)
result = re.sub(r'_+', '_', result)
return result.strip('_').lower()
def render_markdown(content: str) -> str:
md = markdown.Markdown(extensions=['tables', 'fenced_code', 'toc', 'attr_list'])
return md.convert(content)
@router.get("/")
async def documents_list(
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
docs = get_allowed_docs(user["role"])
return templates.TemplateResponse("documents/list.html", _doc_ctx(request, user, docs=docs))
@router.get("/admin/permissions")
async def permissions_page(
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if user["role"] != "owner":
return HTMLResponse(content="Доступ запрещён", status_code=403)
perms = discover_docs()
return templates.TemplateResponse("documents/permissions.html", _doc_ctx(request, user, perms=perms))
@router.post("/admin/permissions")
async def permissions_update(
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if user["role"] != "owner":
return HTMLResponse(content="Доступ запрещён", status_code=403)
form = await request.form()
perms = discover_docs()
for slug in perms.keys():
engineer_view = form.get(f"{slug}_engineer_view") == "on"
engineer_edit = form.get(f"{slug}_engineer_edit") == "on"
technician_view = form.get(f"{slug}_technician_view") == "on"
if engineer_edit and not engineer_view:
engineer_view = True
perms[slug]["permissions"]["engineer"] = {
"view": engineer_view,
"edit": engineer_edit,
"cancel": engineer_edit,
}
perms[slug]["permissions"]["technician"] = {
"view": technician_view,
}
save_permissions(perms)
return RedirectResponse(url="/service/documents/admin/permissions", status_code=302)
@router.post("/admin/create")
async def document_create(
request: Request,
title: str = Form(...),
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if user["role"] != "owner":
return HTMLResponse(content="Доступ запрещён", status_code=403)
slug = transliterate_slug(title)
if not slug:
return HTMLResponse(content="Некорректное название", status_code=400)
docs_dir = settings.DOCS_DIR
os.makedirs(docs_dir, exist_ok=True)
filename = f"{slug}.md"
filepath = os.path.join(docs_dir, filename)
if os.path.exists(filepath):
return HTMLResponse(content="Документ уже существует", status_code=400)
with open(filepath, "w", encoding="utf-8") as f:
f.write(f"# {title}\n\n")
perms = load_permissions()
perms[slug] = {
"title": title,
"filename": filename,
"sort_order": len(perms),
"permissions": {
"engineer": {"view": False, "edit": False, "cancel": False},
"technician": {"view": False},
},
}
save_permissions(perms)
return RedirectResponse(url=f"/service/documents/{slug}/edit", status_code=302)
@router.get("/tech-access")
async def tech_access_page(
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if user["role"] != "engineer":
return HTMLResponse(content="Доступ только для инженеров", status_code=403)
perms = discover_docs()
editable_docs = [
doc for slug, doc in perms.items()
if doc.get("permissions", {}).get("engineer", {}).get("edit", False)
]
return templates.TemplateResponse("documents/tech_access.html", _doc_ctx(request, user, docs=editable_docs, perms=perms))
@router.post("/tech-access")
async def tech_access_update(
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if user["role"] != "engineer":
return HTMLResponse(content="Доступ только для инженеров", status_code=403)
form = await request.form()
perms = discover_docs()
for slug in perms.keys():
if not perms[slug].get("permissions", {}).get("engineer", {}).get("edit", False):
continue
technician_view = form.get(f"{slug}_technician_view") == "on"
perms[slug]["permissions"]["technician"] = {"view": technician_view}
save_permissions(perms)
return RedirectResponse(url="/service/documents/tech-access", status_code=302)
@router.get("/{slug}")
async def document_view(
slug: str,
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if not can_access_doc(user["role"], slug, "view"):
return HTMLResponse(content="Доступ запрещён", status_code=403)
docs_dir = settings.DOCS_DIR
perms = discover_docs()
doc = perms.get(slug)
if not doc:
return HTMLResponse(content="Документ не найден", status_code=404)
filepath = os.path.join(docs_dir, doc["filename"])
if not os.path.exists(filepath):
return HTMLResponse(content="Файл не найден", status_code=404)
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
html_content = render_markdown(content)
return templates.TemplateResponse("documents/view.html", _doc_ctx(request, user,
doc=doc, content=html_content,
can_edit=can_access_doc(user["role"], slug, "edit"),
can_download=user["role"] == "owner",
))
@router.get("/{slug}/edit")
async def document_edit(
slug: str,
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if not can_access_doc(user["role"], slug, "edit"):
return HTMLResponse(content="Доступ запрещён", status_code=403)
docs_dir = settings.DOCS_DIR
perms = discover_docs()
doc = perms.get(slug)
if not doc:
return HTMLResponse(content="Документ не найден", status_code=404)
filepath = os.path.join(docs_dir, doc["filename"])
if not os.path.exists(filepath):
return HTMLResponse(content="Файл не найден", status_code=404)
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
return templates.TemplateResponse("documents/edit.html", _doc_ctx(request, user, doc=doc, content=content))
@router.post("/{slug}/edit")
async def document_update(
slug: str,
request: Request,
content: str = Form(...),
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if not can_access_doc(user["role"], slug, "edit"):
return HTMLResponse(content="Доступ запрещён", status_code=403)
docs_dir = settings.DOCS_DIR
perms = discover_docs()
doc = perms.get(slug)
if not doc:
return HTMLResponse(content="Документ не найден", status_code=404)
filepath = os.path.join(docs_dir, doc["filename"])
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
return RedirectResponse(url=f"/service/documents/{slug}", status_code=302)
@router.get("/{slug}/download")
async def document_download(
slug: str,
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if user["role"] != "owner":
return HTMLResponse(content="Скачивание доступно только владельцу", status_code=403)
from fastapi.responses import FileResponse
docs_dir = settings.DOCS_DIR
perms = discover_docs()
doc = perms.get(slug)
if not doc:
return HTMLResponse(content="Документ не найден", status_code=404)
filepath = os.path.join(docs_dir, doc["filename"])
if not os.path.exists(filepath):
return HTMLResponse(content="Файл не найден", status_code=404)
return FileResponse(filepath, filename=doc["filename"], media_type="text/markdown")