From cc819257a341f0a1e56a25be2e1ae86a325e6d8e Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Sun, 17 Aug 2025 19:27:47 +0300 Subject: [PATCH] Split backend into multiple files --- backend/app.py | 19 +++++ backend/db.py | 6 ++ backend/dependencies.py | 39 +++++++++ backend/main.py | 176 ++-------------------------------------- backend/models.py | 65 +++++++++++++++ backend/utils.py | 33 ++++++++ backend/validation.py | 16 ++++ 7 files changed, 187 insertions(+), 167 deletions(-) create mode 100644 backend/app.py create mode 100644 backend/db.py create mode 100644 backend/dependencies.py create mode 100644 backend/models.py create mode 100644 backend/utils.py create mode 100644 backend/validation.py diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..af096bf --- /dev/null +++ b/backend/app.py @@ -0,0 +1,19 @@ +from fastapi import FastAPI +from fastapi.security import HTTPBearer +from fastapi.middleware.cors import CORSMiddleware + +# Инициализация FastAPI +app = FastAPI(title="PixelChat") + +# CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # В продакшене замените на нужные домены + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# Security для FastAPI +security = HTTPBearer() \ No newline at end of file diff --git a/backend/db.py b/backend/db.py new file mode 100644 index 0000000..834a613 --- /dev/null +++ b/backend/db.py @@ -0,0 +1,6 @@ +from sqlalchemy.orm import sessionmaker +from sqlalchemy import create_engine +from constants import DATABASE_URL + +engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) \ No newline at end of file diff --git a/backend/dependencies.py b/backend/dependencies.py new file mode 100644 index 0000000..3c2249b --- /dev/null +++ b/backend/dependencies.py @@ -0,0 +1,39 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPAuthorizationCredentials +from sqlalchemy.orm import Session +from utils import * +from models import * +from db import SessionLocal +from app import security + + +# Зависимость для получения текущего пользователя +def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + db: Session = Depends(lambda: SessionLocal()) +): + token = credentials.credentials + payload = verify_token(token) + if not payload: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or expired token", + headers={"WWW-Authenticate": "Bearer"}, + ) + user = db.query(User).filter(User.id == payload["user_id"]).first() + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found", + headers={"WWW-Authenticate": "Bearer"}, + ) + return {"user_id": user.id, "username": user.username} + + +# Зависимость для получения сессии БД +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 94c8e44..e09bc4e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,172 +1,14 @@ -from fastapi import FastAPI, Depends, HTTPException, status -from fastapi.middleware.cors import CORSMiddleware -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from sqlalchemy import create_engine, Column, Integer, String, Text, Boolean, DateTime, ForeignKey -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, relationship, Session -from datetime import datetime, timedelta -import jwt -import re -from typing import Optional -from pydantic import BaseModel -import bcrypt +from fastapi import Depends, HTTPException, status +from sqlalchemy.orm import Session +from datetime import datetime from constants import * - -# Инициализация FastAPI -app = FastAPI(title="PixelChat") - -# CORS -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # В продакшене замените на нужные домены - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - -# Настройка базы данных -engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False}) -SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) -Base = declarative_base() - - -# Модели базы данных -class User(Base): - __tablename__ = "user" - - id = Column(Integer, primary_key=True, index=True) - username = Column(String(50), unique=True, nullable=False, index=True) - password_hash = Column(String(200), nullable=False) - online = Column(Boolean, default=False) - last_seen = Column(DateTime, default=datetime.now) - created_at = Column(DateTime, default=datetime.now) - messages = relationship("Message", back_populates="author", lazy="select") - - -class Message(Base): - __tablename__ = "message" - - id = Column(Integer, primary_key=True, index=True) - content = Column(Text, nullable=False) - timestamp = Column(DateTime, default=datetime.now) - user_id = Column(Integer, ForeignKey("user.id"), nullable=False) - is_read = Column(Boolean, default=False) - - author = relationship("User", back_populates="messages") - - -# Валидация данных -def is_valid_username(username: str) -> bool: - if len(username) < 3 or len(username) > 20: - return False - if re.search(r'[\s\u180E\u200B-\u200D\u2060\uFEFF]', username): - return False - return True - - -def is_valid_password(password: str) -> bool: - if len(password) < 5 or len(password) > 50: - return False - if re.search(r'[\s\u180E\u200B-\u200D\u2060\uFEFF]', password): - return False - return True - - -# JWT Helper Functions -def create_token(user_id: int, username: str) -> str: - expire = datetime.utcnow() + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS) - payload = { - "user_id": user_id, - "username": username, - "exp": expire - } - return jwt.encode(payload, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM) - - -def verify_token(token: str) -> Optional[dict]: - try: - payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM]) - return payload - except jwt.ExpiredSignatureError: - return None - except jwt.InvalidTokenError: - return None - - -def verify_password(plain_password: str, hashed_password: str) -> bool: - return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8")) - -def get_password_hash(password: str) -> str: - return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") - - -# Security для FastAPI -security = HTTPBearer() - - -# Зависимость для получения текущего пользователя -def get_current_user( - credentials: HTTPAuthorizationCredentials = Depends(security), - db: Session = Depends(lambda: SessionLocal()) -): - token = credentials.credentials - payload = verify_token(token) - if not payload: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid or expired token", - headers={"WWW-Authenticate": "Bearer"}, - ) - user = db.query(User).filter(User.id == payload["user_id"]).first() - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="User not found", - headers={"WWW-Authenticate": "Bearer"}, - ) - return {"user_id": user.id, "username": user.username} - - -# Зависимость для получения сессии БД -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - - -# Pydantic модели -class LoginRequest(BaseModel): - username: str - password: str - - -class RegisterRequest(BaseModel): - username: str - password: str - confirm_password: str - - -class SendMessageRequest(BaseModel): - content: str - - -class MessageResponse(BaseModel): - id: int - content: str - timestamp: datetime - is_author: bool - is_read: bool - username: str - - class Config: - from_attributes = True - - -# Создание таблиц -Base.metadata.create_all(bind=engine) +from db import * +from models import * +from validation import * +from utils import * +from dependencies import * +from app import * # API маршруты diff --git a/backend/models.py b/backend/models.py new file mode 100644 index 0000000..c193247 --- /dev/null +++ b/backend/models.py @@ -0,0 +1,65 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Column, Integer, String, Text, Boolean, DateTime, ForeignKey +from sqlalchemy.orm import relationship +from datetime import datetime +from db import engine +from pydantic import BaseModel + +Base = declarative_base() + + +# Модели базы данных +class User(Base): + __tablename__ = "user" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String(50), unique=True, nullable=False, index=True) + password_hash = Column(String(200), nullable=False) + online = Column(Boolean, default=False) + last_seen = Column(DateTime, default=datetime.now) + created_at = Column(DateTime, default=datetime.now) + messages = relationship("Message", back_populates="author", lazy="select") + + +class Message(Base): + __tablename__ = "message" + + id = Column(Integer, primary_key=True, index=True) + content = Column(Text, nullable=False) + timestamp = Column(DateTime, default=datetime.now) + user_id = Column(Integer, ForeignKey("user.id"), nullable=False) + is_read = Column(Boolean, default=False) + + author = relationship("User", back_populates="messages") + + +# Pydantic модели +class LoginRequest(BaseModel): + username: str + password: str + + +class RegisterRequest(BaseModel): + username: str + password: str + confirm_password: str + + +class SendMessageRequest(BaseModel): + content: str + + +class MessageResponse(BaseModel): + id: int + content: str + timestamp: datetime + is_author: bool + is_read: bool + username: str + + class Config: + from_attributes = True + + +# Создание таблиц +Base.metadata.create_all(bind=engine) \ No newline at end of file diff --git a/backend/utils.py b/backend/utils.py new file mode 100644 index 0000000..437a324 --- /dev/null +++ b/backend/utils.py @@ -0,0 +1,33 @@ +from datetime import datetime, timedelta +import jwt +from typing import Optional +import bcrypt + +from constants import * + +# JWT Helper Functions +def create_token(user_id: int, username: str) -> str: + expire = datetime.now() + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS) + payload = { + "user_id": user_id, + "username": username, + "exp": expire + } + return jwt.encode(payload, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM) + + +def verify_token(token: str) -> Optional[dict]: + try: + payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM]) + return payload + except jwt.ExpiredSignatureError: + return None + except jwt.InvalidTokenError: + return None + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return bcrypt.checkpw(plain_password.encode("utf-8"), hashed_password.encode("utf-8")) + +def get_password_hash(password: str) -> str: + return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") \ No newline at end of file diff --git a/backend/validation.py b/backend/validation.py new file mode 100644 index 0000000..d145b6f --- /dev/null +++ b/backend/validation.py @@ -0,0 +1,16 @@ +import re + +def is_valid_username(username: str) -> bool: + if len(username) < 3 or len(username) > 20: + return False + if re.search(r'[\s\u180E\u200B-\u200D\u2060\uFEFF]', username): + return False + return True + + +def is_valid_password(password: str) -> bool: + if len(password) < 5 or len(password) > 50: + return False + if re.search(r'[\s\u180E\u200B-\u200D\u2060\uFEFF]', password): + return False + return True \ No newline at end of file