99from typing import List , Dict , Any
1010from contextlib import asynccontextmanager
1111import time
12- from tenacity import retry , stop_after_attempt , wait_exponential , retry_if_exception_type
1312
1413# FastAPI imports
15- from fastapi import FastAPI , HTTPException , Depends , Header
14+ from fastapi import FastAPI , HTTPException , Depends
1615from fastapi .middleware .cors import CORSMiddleware
1716from pydantic import BaseModel
1817from sqlalchemy .orm import Session
19- from typing import Optional
2018
2119# Добавляем корень проекта в PATH
2220sys .path .append (str (Path (__file__ ).parent .parent ))
2624load_dotenv (Path (__file__ ).parent .parent / '.env' )
2725
2826# Импортируем конфигурацию отделов
29- from backend .config .departments import DEPARTMENTS_CONFIG , get_department_config , get_all_departments
27+ from backend .config .departments import get_department_config , get_all_departments
3028
3129# Импортируем database
3230from backend .database import get_db
3331
32+ # Импортируем утилиты безопасности
33+ from backend .auth_utils import (
34+ hash_password ,
35+ verify_password ,
36+ create_access_token ,
37+ get_current_user ,
38+ get_current_admin
39+ )
40+
3441# Импортируем роутеры
3542from backend .routes .chat_sessions import router as chat_sessions_router
3643from backend .routes .instagram import router as instagram_router
@@ -54,8 +61,7 @@ def load_prompt(prompt_file: str) -> str:
5461# Llama Index imports
5562from llama_index .core import (
5663 VectorStoreIndex ,
57- Settings ,
58- QueryBundle
64+ Settings
5965)
6066from llama_index .embeddings .openai import OpenAIEmbedding
6167from llama_index .vector_stores .qdrant import QdrantVectorStore
@@ -142,22 +148,20 @@ class RegisterRequest(BaseModel):
142148
143149# Временное хранилище пользователей (для демо)
144150# В продакшене использовать PostgreSQL
145- # Хранилище активных токенов: token → email
146- ACTIVE_TOKENS = {}
147151
148152USERS_DB = {
149153150154 "id" : 1 ,
151155152- "password" : "progreSS$9281 " , # В продакшене хешировать!
156+ "password" : "$argon2id$v=19$m=65536,t=3,p=4$3tubs7bWuleq9d57D+F8rw$oFPgp4o9tBtXY/gm0qmgelBm0NBMrA3oUbOFdUMdoDI " , # argon2 hash
153157 "first_name" : "Temrjan" ,
154158 "last_name" : "Admin" ,
155159 "role" : "admin"
156160 },
157161158162 "id" : 2 ,
159163160- "password" : "41676$Biotact " , # В продакшене хешировать!
164+ "password" : "$argon2id$v=19$m=65536,t=3,p=4$SIkx5hzjHAMAoFRKKSUkRA$Hii+uqAwmsSyiCMwO1FaN1KEl+HJv+dJrQbQdE4NLBQ " , # argon2 hash
161165 "first_name" : "Ruslan" ,
162166 "last_name" : "Admin" ,
163167 "role" : "admin"
@@ -169,6 +173,12 @@ class RegisterRequest(BaseModel):
169173# Счетчик ID для новых пользователей
170174NEXT_USER_ID = 3
171175
176+ # Rate limiting для login (простая реализация в памяти)
177+ # email -> (timestamp, count)
178+ from collections import defaultdict
179+ import time as time_module
180+ LOGIN_ATTEMPTS = defaultdict (list )
181+
172182
173183# ==================== Инициализация ====================
174184
@@ -264,10 +274,14 @@ async def lifespan(app: FastAPI):
264274# Настройка CORS для фронтенда
265275app .add_middleware (
266276 CORSMiddleware ,
267- allow_origins = ["*" ], # В продакшене указать конкретные домены
277+ allow_origins = [
278+ "https://core.biotact.uz" ,
279+ "http://localhost:5173" , # Для разработки
280+ "http://localhost:3000" # Для разработки
281+ ],
268282 allow_credentials = True ,
269- allow_methods = ["* " ],
270- allow_headers = ["* " ],
283+ allow_methods = ["GET" , "POST" , "PUT" , "DELETE" , "OPTIONS " ],
284+ allow_headers = ["Authorization" , "Content-Type " ],
271285)
272286
273287# Подключаем роутеры
@@ -295,17 +309,17 @@ async def health_check():
295309 qdrant_ok = False
296310 try :
297311 if qdrant_client :
298- collections = qdrant_client .get_collections ()
312+ _collections = qdrant_client .get_collections ()
299313 qdrant_ok = True
300- except :
314+ except Exception :
301315 pass
302316
303317 # Проверяем PostgreSQL
304318 postgres_ok = False
305319 try :
306320 # TODO: добавить проверку PostgreSQL
307321 postgres_ok = True
308- except :
322+ except Exception :
309323 pass
310324
311325 # Проверяем эмбеддинги
@@ -396,7 +410,7 @@ async def get_stats():
396410 if qdrant_client :
397411 collection_info = qdrant_client .get_collection (stats ["collection_name" ])
398412 stats ["vectors_count" ] = collection_info .points_count
399- except :
413+ except Exception :
400414 pass
401415
402416 return stats
@@ -481,29 +495,14 @@ def execute_query_with_retry(query_engine, message: str, max_retries: int = 5):
481495 raise last_exception
482496
483497
484- def get_current_user_dep (authorization : Optional [str ] = Header (None )) -> str :
485- """
486- Dependency для получения текущего пользователя из токена
487- """
488- if not authorization :
489- raise HTTPException (status_code = 401 , detail = "Требуется авторизация" )
490-
491- # Формат: "Bearer <token>" или просто "<token>"
492- token = authorization .replace ("Bearer " , "" ).strip ()
493-
494- # Проверяем токен в ACTIVE_TOKENS
495- email = ACTIVE_TOKENS .get (token )
496- if not email :
497- raise HTTPException (status_code = 401 , detail = "Недействительный или истекший токен" )
498-
499- return email
498+ # Старая функция удалена - используем get_current_user из auth_utils
500499
501500
502501@app .post ("/chat" , response_model = ChatResponse )
503502async def chat (
504503 request : ChatRequest ,
505504 db : Session = Depends (get_db ),
506- current_user : str = Depends (get_current_user_dep )
505+ current_user_data : Dict [ str , Any ] = Depends (get_current_user )
507506):
508507 """Чат с AI ассистентом с поддержкой истории"""
509508
@@ -519,10 +518,13 @@ async def chat(
519518 # Инициализируем ChatService
520519 chat_service = ChatService (db )
521520
521+ # Получаем email текущего пользователя
522+ current_user_email = current_user_data ["email" ]
523+
522524 # Получаем или создаем сессию
523525 if request .session_id :
524526 # Проверяем существующую сессию
525- session = chat_service .get_session (request .session_id , current_user )
527+ session = chat_service .get_session (request .session_id , current_user_email )
526528 if not session :
527529 raise HTTPException (status_code = 404 , detail = "Session not found" )
528530
@@ -537,7 +539,7 @@ async def chat(
537539 else :
538540 # Создаем новую сессию
539541 session = chat_service .create_session (
540- user_identifier = current_user ,
542+ user_identifier = current_user_email ,
541543 department = request .department ,
542544 title = None # Будет установлен автоматически из первого вопроса
543545 )
@@ -619,21 +621,41 @@ async def chat(
619621
620622@app .post ("/auth/login" , response_model = LoginResponse )
621623async def login (request : LoginRequest ):
622- """Вход в систему"""
624+ """Вход в систему с JWT токенами"""
625+
626+ # Rate limiting: максимум 5 попыток в минуту на email
627+ now = time_module .time ()
628+ attempts = LOGIN_ATTEMPTS [request .email ]
623629
624- import secrets
630+ # Удаляем попытки старше 60 секунд
631+ attempts [:] = [t for t in attempts if now - t < 60 ]
632+
633+ if len (attempts ) >= 5 :
634+ raise HTTPException (
635+ status_code = 429 ,
636+ detail = "Слишком много попыток входа. Попробуйте через минуту."
637+ )
638+
639+ # Записываем текущую попытку
640+ attempts .append (now )
625641
626642 # Проверяем пользователя
627643 user = USERS_DB .get (request .email )
628644
629- if not user or user [ "password" ] != request . password :
645+ if not user :
630646 raise HTTPException (status_code = 401 , detail = "Неверный email или пароль" )
631647
632- # Генерируем токен (в продакшене использовать JWT)
633- token = secrets .token_urlsafe (32 )
648+ # Проверяем пароль (хешированный)
649+ if not verify_password (request .password , user ["password" ]):
650+ raise HTTPException (status_code = 401 , detail = "Неверный email или пароль" )
634651
635- # Сохраняем токен → email для аутентификации
636- ACTIVE_TOKENS [token ] = request .email
652+ # Генерируем JWT токен
653+ token = create_access_token (
654+ data = {
655+ "sub" : user ["email" ],
656+ "role" : user ["role" ]
657+ }
658+ )
637659
638660 return LoginResponse (
639661 token = token ,
@@ -666,7 +688,7 @@ async def register(request: RegisterRequest):
666688 PENDING_USERS [request .email ] = {
667689 "id" : user_id ,
668690 "email" : request .email ,
669- "password" : request .password , # В продакшене хешировать!
691+ "password" : hash_password ( request .password ) , # Хешируем пароль
670692 "first_name" : request .first_name ,
671693 "last_name" : request .last_name ,
672694 "phone_number" : request .phone_number ,
@@ -684,7 +706,7 @@ async def register(request: RegisterRequest):
684706
685707
686708@app .get ("/auth/admin/users/pending" )
687- async def get_pending_users ():
709+ async def get_pending_users (admin_user : Dict [ str , Any ] = Depends ( get_current_admin ) ):
688710 """Получить список пользователей на модерацию (для админа)"""
689711
690712 pending_list = [
@@ -708,7 +730,7 @@ async def get_pending_users():
708730
709731
710732@app .post ("/auth/admin/users/{user_id}/approve" )
711- async def approve_user (user_id : int ):
733+ async def approve_user (user_id : int , admin_user : Dict [ str , Any ] = Depends ( get_current_admin ) ):
712734 """Одобрить пользователя (для админа)"""
713735
714736 # Находим пользователя в pending
@@ -749,7 +771,7 @@ async def approve_user(user_id: int):
749771
750772
751773@app .post ("/auth/admin/users/{user_id}/reject" )
752- async def reject_user (user_id : int ):
774+ async def reject_user (user_id : int , admin_user : Dict [ str , Any ] = Depends ( get_current_admin ) ):
753775 """Отклонить заявку пользователя (для админа)"""
754776
755777 # Находим пользователя в pending
@@ -868,14 +890,13 @@ async def run_indexing():
868890# ==================== STREAMING CHAT ENDPOINT ====================
869891
870892from fastapi .responses import StreamingResponse
871- import asyncio
872893from openai import OpenAI as OpenAIClient
873894
874895@app .post ("/chat/stream" )
875896async def chat_stream (
876897 request : ChatRequest ,
877898 db : Session = Depends (get_db ),
878- current_user : str = Depends (get_current_user_dep )
899+ current_user_data : Dict [ str , Any ] = Depends (get_current_user )
879900):
880901 """
881902 Streaming чат с AI ассистентом.
@@ -891,17 +912,20 @@ async def chat_stream(
891912 # Инициализируем ChatService
892913 chat_service = ChatService (db )
893914
915+ # Получаем email текущего пользователя
916+ current_user_email = current_user_data ["email" ]
917+
894918 # Получаем или создаем сессию
895919 if request .session_id :
896- session = chat_service .get_session (request .session_id , current_user )
920+ session = chat_service .get_session (request .session_id , current_user_email )
897921 if not session :
898922 raise HTTPException (status_code = 404 , detail = "Session not found" )
899923 if session .department != request .department :
900924 raise HTTPException (status_code = 400 , detail = "Department mismatch" )
901925 session_id = request .session_id
902926 else :
903927 session = chat_service .create_session (
904- user_identifier = current_user ,
928+ user_identifier = current_user_email ,
905929 department = request .department ,
906930 title = None
907931 )
@@ -939,7 +963,7 @@ async def chat_stream(
939963 })
940964
941965 # Формируем полный промпт
942- full_prompt = f"""{ system_prompt }
966+ _full_prompt = f"""{ system_prompt }
943967
944968Контекст из базы знаний:
945969{ rag_context }
0 commit comments