refactor(web_api): fix import paths, modularize routers and decompose agent pipeline
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
===============================================================================
|
||||
FILE: modules/web_api/routers/admin.py
|
||||
ROLE: Администрирование пользователей и прав доступа.
|
||||
===============================================================================
|
||||
"""
|
||||
|
||||
# ANCHOR[ADMIN_ROUTER_IMPORTS]
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .auth import get_current_user, get_db, pwd_context
|
||||
|
||||
router = APIRouter(prefix="/api/v1/admin", tags=["admin"])
|
||||
|
||||
# ANCHOR[ADMIN_SCHEMAS]
|
||||
class CreateUserRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
full_name: Optional[str] = None
|
||||
is_admin: Optional[bool] = False
|
||||
|
||||
# ANCHOR[ADMIN_ENDPOINTS]
|
||||
@router.get("/users")
|
||||
def list_users(current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
if not current_user["is_admin"]:
|
||||
raise HTTPException(status_code=403, detail="Доступ запрещен. Только для администратора.")
|
||||
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT id, username, full_name, is_admin, created_at FROM users ORDER BY id ASC")
|
||||
users = [dict(r) for r in cursor.fetchall()]
|
||||
conn.close()
|
||||
return users
|
||||
|
||||
@router.post("/users")
|
||||
def create_user(req: CreateUserRequest, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
if not current_user["is_admin"]:
|
||||
raise HTTPException(status_code=403, detail="Доступ запрещен. Только для администратора.")
|
||||
|
||||
username = req.username.strip().lower()
|
||||
if not username or not req.password:
|
||||
raise HTTPException(status_code=400, detail="Заполните имя пользователя и пароль")
|
||||
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT id FROM users WHERE username = ?", (username,))
|
||||
if cursor.fetchone():
|
||||
conn.close()
|
||||
raise HTTPException(status_code=400, detail="Пользователь с таким именем уже существует")
|
||||
|
||||
pwd_hash = pwd_context.hash(req.password)
|
||||
full_name = req.full_name.strip() if req.full_name else None
|
||||
is_admin = 1 if req.is_admin else 0
|
||||
|
||||
cursor.execute(
|
||||
"INSERT INTO users (username, password_hash, full_name, is_admin) VALUES (?, ?, ?, ?)",
|
||||
(username, pwd_hash, full_name, is_admin)
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logging.info(f"Создан пользователь: {username} (admin={is_admin}) админом {current_user['username']}")
|
||||
return {"status": "success", "message": f"Пользователь {username} создан"}
|
||||
|
||||
@router.delete("/users/{user_id}")
|
||||
def delete_user(user_id: int, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
if not current_user["is_admin"]:
|
||||
raise HTTPException(status_code=403, detail="Доступ запрещен. Только для администратора.")
|
||||
|
||||
if user_id == current_user["id"]:
|
||||
raise HTTPException(status_code=400, detail="Нельзя удалить самого себя")
|
||||
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM users WHERE id = ?", (user_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logging.info(f"Удален пользователь ID: {user_id}")
|
||||
return {"status": "success", "message": "Пользователь удален"}
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
===============================================================================
|
||||
FILE: modules/web_api/routers/auth.py
|
||||
ROLE: Аутентификация, валидация JWT-токенов и управление паролями.
|
||||
===============================================================================
|
||||
"""
|
||||
|
||||
# ANCHOR[AUTH_ROUTER_IMPORTS]
|
||||
import sqlite3
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
import jwt
|
||||
from passlib.context import CryptContext
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from pydantic import BaseModel
|
||||
|
||||
from llm.db_tools import DB_PATH
|
||||
|
||||
JWT_SECRET = "scud_jwt_secret_key_2026_orion_ai_super_secure"
|
||||
ALGORITHM = "HS256"
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
security = HTTPBearer()
|
||||
|
||||
router = APIRouter(prefix="/api/v1/auth", tags=["auth"])
|
||||
|
||||
# ANCHOR[AUTH_DB_HELPERS]
|
||||
def get_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def create_access_token(user_id: int, username: str, is_admin: bool) -> str:
|
||||
payload = {
|
||||
"sub": str(user_id),
|
||||
"username": username,
|
||||
"is_admin": is_admin,
|
||||
"exp": datetime.utcnow() + timedelta(days=30)
|
||||
}
|
||||
return jwt.encode(payload, JWT_SECRET, algorithm=ALGORITHM)
|
||||
|
||||
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> Dict[str, Any]:
|
||||
try:
|
||||
token = credentials.credentials
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=[ALGORITHM])
|
||||
user_id = int(payload.get("sub"))
|
||||
username = payload.get("username")
|
||||
is_admin = bool(payload.get("is_admin", False))
|
||||
return {"id": user_id, "username": username, "is_admin": is_admin}
|
||||
except Exception as e:
|
||||
logging.warning(f"Auth error: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Недействительный или просроченный токен авторизации",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# ANCHOR[AUTH_SCHEMAS]
|
||||
class AuthRequest(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
old_password: str
|
||||
new_password: str
|
||||
|
||||
# ANCHOR[AUTH_ENDPOINTS]
|
||||
@router.post("/login")
|
||||
def login(req: AuthRequest):
|
||||
username = req.username.strip().lower()
|
||||
logging.info(f"===> Попытка входа для пользователя: {username}")
|
||||
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT id, username, password_hash, is_admin FROM users WHERE username = ?", (username,))
|
||||
user = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if not user or not pwd_context.verify(req.password, user["password_hash"]):
|
||||
logging.warning(f"===> Ошибка: Неверный логин или пароль для {username}")
|
||||
raise HTTPException(status_code=401, detail="Неверное имя пользователя или пароль")
|
||||
|
||||
is_admin = bool(user["is_admin"]) or (user["username"] == "puh")
|
||||
token = create_access_token(user["id"], user["username"], is_admin)
|
||||
logging.info(f"===> УСПЕХ: Авторизован пользователь {username}")
|
||||
|
||||
return {"status": "success", "token": token, "username": user["username"], "is_admin": is_admin}
|
||||
|
||||
@router.post("/change-password")
|
||||
def change_password(req: ChangePasswordRequest, current_user: Dict[str, Any] = Depends(get_current_user)):
|
||||
if not req.new_password or len(req.new_password) < 4:
|
||||
raise HTTPException(status_code=400, detail="Новый пароль должен содержать минимум 4 символа")
|
||||
|
||||
conn = get_db()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT password_hash FROM users WHERE id = ?", (current_user["id"],))
|
||||
user = cursor.fetchone()
|
||||
|
||||
if not user or not pwd_context.verify(req.old_password, user["password_hash"]):
|
||||
conn.close()
|
||||
raise HTTPException(status_code=400, detail="Неверный старый пароль")
|
||||
|
||||
new_hash = pwd_context.hash(req.new_password)
|
||||
cursor.execute("UPDATE users SET password_hash = ? WHERE id = ?", (new_hash, current_user["id"]))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logging.info(f"Пароль успешно изменен для пользователя ID: {current_user['id']}")
|
||||
return {"status": "success", "message": "Пароль успешно изменен"}
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
===============================================================================
|
||||
FILE: modules/web_api/routers/chat.py
|
||||
ROLE: Маршрутизация диалогов с LLM (авторизованный и гостевой чаты).
|
||||
===============================================================================
|
||||
"""
|
||||
|
||||
# ANCHOR[CHAT_ROUTER_IMPORTS]
|
||||
from typing import Optional, Dict, Any
|
||||
from fastapi import APIRouter, Depends, UploadFile, File, Form
|
||||
|
||||
from .auth import get_current_user
|
||||
from llm.agent import process_chat_message
|
||||
from llm.file_parser import extract_text_from_file
|
||||
|
||||
router = APIRouter(prefix="/api/v1/chat", tags=["chat"])
|
||||
|
||||
# ANCHOR[CHAT_ENDPOINTS]
|
||||
@router.post("")
|
||||
async def chat_endpoint(
|
||||
session_id: str = Form("web_session_main"),
|
||||
message: str = Form(""),
|
||||
file: Optional[UploadFile] = File(default=None),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Диалог авторизованного пользователя с агентом."""
|
||||
parsed_file = {"text": "", "image_b64": None}
|
||||
if file and file.filename:
|
||||
file_bytes = await file.read()
|
||||
parsed_file = extract_text_from_file(file_bytes, file.filename)
|
||||
|
||||
reply, history, action_type = process_chat_message(
|
||||
user_id=current_user["id"],
|
||||
user_message=message,
|
||||
file_context=parsed_file["text"],
|
||||
image_b64=parsed_file["image_b64"],
|
||||
session_id=session_id
|
||||
)
|
||||
return {"reply": reply, "history": history, "action_type": action_type}
|
||||
|
||||
@router.post("/guest")
|
||||
async def guest_chat_endpoint(
|
||||
session_id: str = Form("web_session_main"),
|
||||
message: str = Form(""),
|
||||
file: Optional[UploadFile] = File(default=None)
|
||||
):
|
||||
"""Гостевой диалог (user_id=0)."""
|
||||
parsed_file = {"text": "", "image_b64": None}
|
||||
if file and file.filename:
|
||||
file_bytes = await file.read()
|
||||
parsed_file = extract_text_from_file(file_bytes, file.filename)
|
||||
|
||||
reply, history, action_type = process_chat_message(
|
||||
user_id=0,
|
||||
user_message=message,
|
||||
file_context=parsed_file["text"],
|
||||
image_b64=parsed_file["image_b64"],
|
||||
session_id=session_id
|
||||
)
|
||||
return {"reply": reply, "history": history, "action_type": action_type}
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
===============================================================================
|
||||
FILE: modules/web_api/routers/tasks.py
|
||||
ROLE: REST API управления задачами (GET / POST / PATCH / DELETE).
|
||||
===============================================================================
|
||||
"""
|
||||
|
||||
# ANCHOR[TASKS_ROUTER_IMPORTS]
|
||||
from typing import Dict, Any, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from .auth import get_current_user
|
||||
from llm.db_tools import (
|
||||
db_get_tasks,
|
||||
db_add_task,
|
||||
db_update_task_status,
|
||||
db_delete_task
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/api/v1/tasks", tags=["tasks"])
|
||||
|
||||
# ANCHOR[TASKS_SCHEMAS]
|
||||
class CreateTaskRequest(BaseModel):
|
||||
title: str
|
||||
priority: Optional[str] = "MEDIUM"
|
||||
module: Optional[str] = "general"
|
||||
due_date: Optional[str] = None
|
||||
|
||||
class UpdateTaskRequest(BaseModel):
|
||||
status: Optional[str] = "COMPLETED"
|
||||
due_date: Optional[str] = None
|
||||
|
||||
# ANCHOR[TASKS_ENDPOINTS]
|
||||
@router.get("")
|
||||
def get_tasks(user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Получить список всех задач текущего авторизованного пользователя."""
|
||||
return db_get_tasks(user_id=user["id"])
|
||||
|
||||
@router.post("")
|
||||
def create_task_endpoint(req: CreateTaskRequest, user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Прямое создание задачи."""
|
||||
res = db_add_task(
|
||||
user_id=user["id"],
|
||||
module=req.module or "general",
|
||||
title=req.title.strip(),
|
||||
priority=req.priority or "MEDIUM",
|
||||
due_date=req.due_date
|
||||
)
|
||||
return res
|
||||
|
||||
@router.patch("/{task_id}")
|
||||
def update_task_endpoint(task_id: str, req: UpdateTaskRequest, user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Прямое обновление статуса и срока задачи."""
|
||||
res = db_update_task_status(
|
||||
user_id=user["id"],
|
||||
task_id=task_id,
|
||||
status=req.status or "COMPLETED",
|
||||
due_date=req.due_date
|
||||
)
|
||||
if "error" in res:
|
||||
raise HTTPException(status_code=404, detail=res["error"])
|
||||
return res
|
||||
|
||||
@router.delete("/{task_id}")
|
||||
def delete_task_endpoint(task_id: str, user: Dict[str, Any] = Depends(get_current_user)):
|
||||
"""Прямое удаление задачи."""
|
||||
res = db_delete_task(user_id=user["id"], task_id=task_id)
|
||||
if "error" in res:
|
||||
raise HTTPException(status_code=404, detail=res["error"])
|
||||
return res
|
||||
Reference in New Issue
Block a user