v1.7.1: fix all P1-P4, C1-C6, H1-H6, M1-M7, L1-L3 from error.md — async migrations, CSS variables, SQL validation, event loop, prompt injection, VCF parsing, race conditions, unused imports, gitignore

This commit is contained in:
2026-05-29 03:46:41 +03:00
parent 72b6879f4b
commit 54af5892d5
21 changed files with 406 additions and 156 deletions
+37 -31
View File
@@ -1,15 +1,13 @@
from __future__ import annotations
import logging
import json
from contextlib import asynccontextmanager
from typing import Optional
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
from sqlalchemy import select, text
from app.config import settings
from app.database import engine, async_session
from app.models import Base
from app.models import Base, BotConversation
from app.max_api import max_api
from app.settings_cache import settings_cache
from app.yandex_gpt import yandex_gpt
@@ -72,32 +70,48 @@ async def _apply_migrations():
logger.warning(f"Migration note: {e}")
# BotUser new columns
_add_column_if_not_exists(conn, "bot_users", "patronymic", "VARCHAR(255)")
_add_column_if_not_exists(conn, "bot_users", "email", "VARCHAR(255)")
_add_column_if_not_exists(conn, "bot_users", "organization", "VARCHAR(255)")
_add_column_if_not_exists(conn, "bot_users", "address", "TEXT")
_add_column_if_not_exists(conn, "bot_users", "vcf_raw", "TEXT")
_add_column_if_not_exists(conn, "bot_users", "contact_hash", "VARCHAR(255)")
_add_column_if_not_exists(conn, "bot_users", "phone_verified", "BOOLEAN DEFAULT FALSE")
_add_column_if_not_exists(conn, "bot_users", "email_verified", "BOOLEAN DEFAULT FALSE")
_add_column_if_not_exists(conn, "bot_users", "last_interaction", "TIMESTAMP")
_add_column_if_not_exists(conn, "bot_users", "total_conversations", "INTEGER DEFAULT 0")
_add_column_if_not_exists(conn, "bot_users", "total_tickets", "INTEGER DEFAULT 0")
await _add_column_if_not_exists(conn, "bot_users", "patronymic", "VARCHAR(255)")
await _add_column_if_not_exists(conn, "bot_users", "email", "VARCHAR(255)")
await _add_column_if_not_exists(conn, "bot_users", "organization", "VARCHAR(255)")
await _add_column_if_not_exists(conn, "bot_users", "address", "TEXT")
await _add_column_if_not_exists(conn, "bot_users", "vcf_raw", "TEXT")
await _add_column_if_not_exists(conn, "bot_users", "contact_hash", "VARCHAR(255)")
await _add_column_if_not_exists(conn, "bot_users", "phone_verified", "BOOLEAN DEFAULT FALSE")
await _add_column_if_not_exists(conn, "bot_users", "email_verified", "BOOLEAN DEFAULT FALSE")
await _add_column_if_not_exists(conn, "bot_users", "last_interaction", "TIMESTAMP")
await _add_column_if_not_exists(conn, "bot_users", "total_conversations", "INTEGER DEFAULT 0")
await _add_column_if_not_exists(conn, "bot_users", "total_tickets", "INTEGER DEFAULT 0")
def _add_column_if_not_exists(conn, table: str, column: str, col_type: str):
async def _add_column_if_not_exists(conn, table: str, column: str, col_type: str):
from sqlalchemy import text as sa_text
_validate_sql_name(table)
_validate_sql_name(column)
_validate_sql_type(col_type)
try:
conn.execute(sa_text(f"""
await conn.execute(sa_text(f"""
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name='{table}' AND column_name='{column}'
) THEN
ALTER TABLE {table} ADD COLUMN {column} {col_type};
ALTER TABLE "{table}" ADD COLUMN "{column}" {col_type};
END IF;
END $$;
"""))
def _validate_sql_name(name: str) -> None:
import re
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_ ]*$', name):
raise ValueError(f"Invalid SQL identifier: {name}")
def _validate_sql_type(col_type: str) -> None:
import re
allowed = r'^(VARCHAR\(\d+\)|TEXT|INTEGER|BOOLEAN( DEFAULT (TRUE|FALSE))?|TIMESTAMP)$'
if not re.match(allowed, col_type.strip()):
raise ValueError(f"Invalid SQL type: {col_type}")
except Exception as e:
logger.warning(f"Migration add column {table}.{column}: {e}")
@@ -457,8 +471,8 @@ async def api_update_ticket(ticket_id: int, request: Request):
if not ticket:
return JSONResponse({"ok": False, "error": "Not found"}, status_code=404)
old_status = ticket.status
if status:
if status and status != ticket.status:
old_status = ticket.status
ticket.status = status
db.add(BotTicketStatus(
ticket_id=ticket_id,
@@ -473,17 +487,9 @@ async def api_update_ticket(ticket_id: int, request: Request):
def _extract_phone_from_vcf(vcf_info: str) -> str:
import re
match = re.search(r"TEL[^:]*:(\+?\d+)", vcf_info)
match = re.search(r"TEL[^:]*:(\+?\d[\d\s\-()]*)", vcf_info)
if match:
return match.group(1)
return ""
def _extract_name_from_vcf(vcf_info: str) -> str:
import re
match = re.search(r"FN:(.+)", vcf_info)
if match:
return match.group(1).strip()
return re.sub(r"[\s\-()]", "", match.group(1).strip())
return ""
@@ -513,7 +519,7 @@ def _parse_vcf(vcf_info: str) -> dict:
result["first_name"] = parts[0]
# N: structured name (last;first;middle;prefix;suffix)
n_match = re.search(r"N(?:;[^:]*)?:([^;\r\n]+)", vcf_info)
n_match = re.search(r"N(?:;[^:]*)?:([^\r\n]+)", vcf_info)
if n_match:
n_parts = n_match.group(1).split(";")
if len(n_parts) >= 1 and n_parts[0]:
@@ -535,7 +541,7 @@ def _parse_vcf(vcf_info: str) -> dict:
result["email"] = email_match.group(1).strip()
# ORG
org_match = re.search(r"ORG[^:]*:([^\r\n;]+)", vcf_info)
org_match = re.search(r"ORG[^:]*:([^\r\n]+)", vcf_info)
if org_match:
result["organization"] = org_match.group(1).strip()