Files
scud_context_api/main.py
T

251 lines
9.8 KiB
Python

import json
import sqlite3
import logging
import urllib.request
import os
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
import jwt
from passlib.context import CryptContext
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from pydantic import BaseModel
from llm.agent import process_chat_message
from llm.db_tools import db_get_tasks, DB_PATH
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[logging.StreamHandler()]
)
JWT_SECRET = "scud_jwt_secret_key_2026_orion_ai_super_secure"
ALGORITHM = "HS256"
OLLAMA_URL = "http://192.168.11.3:11434/api/chat"
MODEL_NAME = "qwen2.5:14b"
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
security = HTTPBearer()
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"},
)
app = FastAPI(title="SCUD Orion AI Context API")
if os.path.exists("static"):
app.mount("/static", StaticFiles(directory="static"), name="static")
class AuthRequest(BaseModel):
username: str
password: str
class CreateUserRequest(BaseModel):
username: str
password: str
full_name: Optional[str] = None
is_admin: Optional[bool] = False
class ChangePasswordRequest(BaseModel):
old_password: str
new_password: str
class ChatRequest(BaseModel):
session_id: str
message: str
# === API МАРШРУТЫ ===
@app.get("/")
def read_root():
return FileResponse("static/index.html")
@app.post("/api/v1/auth/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:
logging.warning(f"===> Ошибка: Пользователь {username} не найден")
raise HTTPException(status_code=401, detail="Неверное имя пользователя или пароль")
if 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}
@app.post("/api/v1/auth/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": "Пароль успешно изменен"}
@app.get("/api/v1/admin/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
@app.post("/api/v1/admin/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} создан"}
@app.delete("/api/v1/admin/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": "Пользователь удален"}
@app.get("/api/v1/tasks")
def get_tasks(user: Dict[str, Any] = Depends(get_current_user)):
return db_get_tasks(user_id=user["id"])
@app.post("/api/v1/chat")
def chat_endpoint(req: ChatRequest, user: Dict[str, Any] = Depends(get_current_user)):
reply, _ = process_chat_message(user_id=user["id"], user_message=req.message)
return {"reply": reply}
@app.post("/api/v1/chat/guest")
def guest_chat_endpoint(req: ChatRequest):
payload = {
"model": MODEL_NAME,
"messages": [
{"role": "system", "content": "Ты — полезный ИИ-ассистент. Отвечай на вопросы пользователя четко и по существу."},
{"role": "user", "content": req.message}
],
"stream": False,
"options": {"num_predict": 2048, "temperature": 0.3}
}
try:
req_ollama = urllib.request.Request(
OLLAMA_URL,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"}
)
with urllib.request.urlopen(req_ollama) as response:
res_data = json.loads(response.read().decode("utf-8"))
reply = res_data.get("message", {}).get("content", "").strip()
return {"reply": reply}
except Exception as e:
return {"reply": f"Ошибка связи с локальной нейросетью: {e}"}
# === СТРОГО В КОНЦЕ: ФОЛЛБЭК СТАТИКИ ===
@app.get("/{file_path:path}")
def serve_static_fallback(file_path: str):
clean_path = file_path.lstrip("/")
target = os.path.join("static", clean_path)
if os.path.isfile(target):
return FileResponse(target)
filename = os.path.basename(clean_path)
target_js = os.path.join("static/js", filename)
if filename.endswith(".js") and os.path.isfile(target_js):
return FileResponse(target_js, media_type="application/javascript")
target_css = os.path.join("static/css", filename)
if filename.endswith(".css") and os.path.isfile(target_css):
return FileResponse(target_css, media_type="text/css")
raise HTTPException(status_code=404, detail="File not found")