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")