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
This commit is contained in:
+163
-154
@@ -6,6 +6,7 @@ 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
|
||||
@@ -15,6 +16,22 @@ 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',
|
||||
@@ -41,157 +58,7 @@ async def documents_list(
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
docs = get_allowed_docs(user["role"])
|
||||
return templates.TemplateResponse("documents/list.html", {
|
||||
"request": request, "user": user, "docs": docs,
|
||||
})
|
||||
|
||||
|
||||
@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", {
|
||||
"request": request, "user": 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", {
|
||||
"request": request, "user": 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("/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", {
|
||||
"request": request, "user": 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}/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")
|
||||
return templates.TemplateResponse("documents/list.html", _doc_ctx(request, user, docs=docs))
|
||||
|
||||
|
||||
@router.get("/admin/permissions")
|
||||
@@ -204,9 +71,7 @@ async def permissions_page(
|
||||
return HTMLResponse(content="Доступ запрещён", status_code=403)
|
||||
|
||||
perms = discover_docs()
|
||||
return templates.TemplateResponse("documents/permissions.html", {
|
||||
"request": request, "user": user, "perms": perms,
|
||||
})
|
||||
return templates.TemplateResponse("documents/permissions.html", _doc_ctx(request, user, perms=perms))
|
||||
|
||||
|
||||
@router.post("/admin/permissions")
|
||||
@@ -280,3 +145,147 @@ async def document_create(
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user