diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/auth/jwt.py | 2 | ||||
| -rw-r--r-- | app/main.py | 2 | ||||
| -rw-r--r-- | app/models/user.py | 3 | ||||
| -rw-r--r-- | app/routes/auth.py | 37 | ||||
| -rw-r--r-- | app/routes/me.py | 54 | ||||
| -rw-r--r-- | app/routes/register.py | 93 | ||||
| -rw-r--r-- | app/utils/cors.py | 27 |
7 files changed, 123 insertions, 95 deletions
diff --git a/app/auth/jwt.py b/app/auth/jwt.py index 2d2aac5..cf8b732 100644 --- a/app/auth/jwt.py +++ b/app/auth/jwt.py @@ -15,7 +15,7 @@ if not JWT_SECRET: JWT_ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 60 -REFRESH_TOKEN_EXPIRE_DAYS = 7 +REFRESH_TOKEN_EXPIRE_DAYS = 30 def create_access_token(data: Dict, expires_delta: Optional[timedelta] = None) -> str: diff --git a/app/main.py b/app/main.py index d2925ed..8506b37 100644 --- a/app/main.py +++ b/app/main.py @@ -7,11 +7,13 @@ from app.routes import router as api_router from app.utils.create_tables import init_db from app.utils.db import engine from app.utils.logger_cfg import logger +from app.utils.cors import setup_cors app_start_time = time.perf_counter() logger.debug("App start timestamp recorded") app = FastAPI() +setup_cors(app) logger.info("FastAPI application instance created") logger.debug("FastAPI instance created successfully") diff --git a/app/models/user.py b/app/models/user.py index 76ba3f9..8dbe419 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -17,7 +17,7 @@ class User(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True) username: Mapped[str] = mapped_column(String(20), unique=True, nullable=False) password: Mapped[str | None] = mapped_column(String(255), nullable=True) - email: Mapped[str | None] = mapped_column(String(120), unique=True, nullable=True) + email: Mapped[str] = mapped_column(String(120), unique=True, nullable=False) google_id: Mapped[str | None] = mapped_column( String(255), unique=True, nullable=True ) @@ -26,6 +26,7 @@ class User(Base): premium: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) is_banned: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) is_moderator: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + token_version: Mapped[int] = mapped_column(Integer, default=0, nullable=False) @classmethod async def get_user_by_email( diff --git a/app/routes/auth.py b/app/routes/auth.py index aa68c52..a2de6db 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -1,19 +1,19 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Response from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy.ext.asyncio import AsyncSession -from app.utils.logger_cfg import logger - from app.auth.jwt import create_access_token, create_refresh_token from app.models.user import User from app.utils.db import get_async_session from app.utils.hash_cfg import verify_password +from app.utils.logger_cfg import logger router = APIRouter(tags=["auth"]) @router.post("/login") async def login( + response: Response, form_data: OAuth2PasswordRequestForm = Depends(), session: AsyncSession = Depends(get_async_session), ): @@ -25,13 +25,28 @@ async def login( logger.warning("Login failed | username/email={}", form_data.username) raise HTTPException(status_code=401, detail="Invalid credentials") - access_token = create_access_token({"sub": str(user.id)}) - refresh_token = create_refresh_token({"sub": str(user.id)}) + access_token = create_access_token( + {"sub": str(user.id), "token_version": user.token_version} + ) + refresh_token = create_refresh_token( + {"sub": str(user.id), "token_version": user.token_version} + ) + response.set_cookie( + key="access_token", + value=access_token, + httponly=True, + secure=False, + samesite="lax", + max_age=60 * 60, + ) + response.set_cookie( + key="refresh_token", + value=refresh_token, + httponly=True, + secure=True, + samesite="lax", + max_age=30 * 24 * 60 * 60, + ) logger.info("User logged in | id={} username={}", user.id, user.username) - - return { - "access_token": access_token, - "refresh_token": refresh_token, - "token_type": "bearer", - } + return {"message": "Logged in successfully"} diff --git a/app/routes/me.py b/app/routes/me.py index a09453c..03d0daa 100644 --- a/app/routes/me.py +++ b/app/routes/me.py @@ -1,48 +1,40 @@ -from fastapi import APIRouter, Depends, HTTPException -from fastapi.security import OAuth2PasswordBearer +from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.ext.asyncio import AsyncSession from app.auth.jwt import decode_token from app.models.user import User from app.utils.db import get_async_session -from app.utils.logger_cfg import logger -router = APIRouter() -oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") +router = APIRouter(tags=["auth"]) -@router.get("/me") -async def read_current_user( - token: str = Depends(oauth2_scheme), +async def get_current_user_from_cookie( + request: Request, session: AsyncSession = Depends(get_async_session), ): - + token = request.cookies.get("access_token") if not token: - logger.warning("No token provided in /me request") raise HTTPException(status_code=401, detail="Unauthorized") - try: - payload = decode_token(token) - user_id = int(payload.get("sub")) - user = await User.get_user_by_id(user_id, session=session) + payload = decode_token(token) + user_id = int(payload.get("sub")) + user = await User.get_user_by_id(user_id, session=session) - if not user: - logger.warning("User not found in /me | id={}", user_id) - raise HTTPException(status_code=404, detail="User not found") + if not user: + raise HTTPException(status_code=404, detail="User not found") + if user.token_version != payload.get("token_version"): + raise HTTPException(status_code=401, detail="Token revoked") - logger.info("User accessed /me | id={} username={}", user.id, user.username) + return user - user_data = { - "id": user.id, - "username": user.username, - "email": user.email, - "premium": user.premium, - "is_banned": user.is_banned, - "is_moderator": user.is_moderator, - } - logger.debug("Returning /me data: {}", user_data) - return user_data - except ValueError as e: - logger.warning("Invalid token in /me request: {}", e) - raise HTTPException(status_code=401, detail="Invalid token") +@router.get("/me") +async def read_current_user(user: User = Depends(get_current_user_from_cookie)): + return { + "id": user.id, + "username": user.username, + "email": user.email, + "premium": user.premium, + "is_banned": user.is_banned, + "is_moderator": user.is_moderator, + } diff --git a/app/routes/register.py b/app/routes/register.py index fb8ec3d..ffcd336 100644 --- a/app/routes/register.py +++ b/app/routes/register.py @@ -1,10 +1,11 @@ import re from typing import Optional -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Response from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select +from app.auth.jwt import create_access_token, create_refresh_token from app.models.user import User from app.schemas.user import UserCreate, UserRead from app.utils.db import get_async_session @@ -16,21 +17,15 @@ router = APIRouter(tags=["auth"]) @router.post("/register", response_model=UserRead) async def register_user( - user: UserCreate, session: AsyncSession = Depends(get_async_session) + user: UserCreate, + response: Response, + session: AsyncSession = Depends(get_async_session), ): logger.debug("Register request received") email: Optional[str] = user.email.strip() if user.email else None - logger.debug("Normalized email value: {}", email) - - logger.info( - "Registration attempt | username={} email={}", - user.username, - email, - ) - - logger.debug("Validating password complexity") + logger.info("Registration attempt | username={} email={}", user.username, email) if not ( re.search(r"[A-Za-z]", user.password) @@ -41,14 +36,18 @@ async def register_user( "Registration failed | password complexity requirement not met | username={}", user.username, ) + raise HTTPException( + status_code=400, + detail={ + "field": "password", + "message": "Попробуйте сочетание букв, цифр и символов.", + }, + ) - logger.debug("Checking if username already exists") result = await session.execute(select(User).where(User.username == user.username)) - existing_username = result.scalars().first() - if existing_username: + if result.scalars().first(): logger.warning( - "Registration failed | username already exists | username={}", - user.username, + "Registration failed | username already exists | username={}", user.username ) raise HTTPException( status_code=400, @@ -56,55 +55,23 @@ async def register_user( ) if email: - logger.debug("Checking if email already exists") result = await session.execute(select(User).where(User.email == email)) - existing_email = result.scalars().first() - if existing_email: + if result.scalars().first(): logger.warning( - "Registration failed | email already exists | email={}", - email, + "Registration failed | email already exists | email={}", email ) raise HTTPException( status_code=400, detail={"field": "email", "message": "Адрес уже занят."}, ) - logger.debug("Starting password hashing") - hashed_password = hash_password(user.password) - logger.debug("Password hashing completed") - - logger.debug("Creating new user") - - new_user = User( - username=user.username, - email=email, - password=hashed_password, - ) - - logger.debug("User model created | username={}", user.username) - - logger.debug("Adding user to session") - + new_user = User(username=user.username, email=email, password=hashed_password) session.add(new_user) - - logger.debug("Preparing to commit database transaction") - await session.commit() - - logger.debug("Transaction committed successfully") - - logger.debug("Refreshing user instance from database") - await session.refresh(new_user) - logger.debug( - "User instance refreshed | id={} username={}", - new_user.id, - new_user.username, - ) - logger.success( "User successfully registered | id={} username={} email={}", new_user.id, @@ -112,4 +79,28 @@ async def register_user( new_user.email, ) + access_token = create_access_token( + {"sub": str(new_user.id), "token_version": new_user.token_version} + ) + refresh_token = create_refresh_token( + {"sub": str(new_user.id), "token_version": new_user.token_version} + ) + + response.set_cookie( + key="access_token", + value=access_token, + httponly=True, + secure=False, + samesite="lax", + max_age=60 * 60, + ) + response.set_cookie( + key="refresh_token", + value=refresh_token, + httponly=True, + secure=False, + samesite="lax", + max_age=30 * 24 * 60 * 60, + ) + return new_user diff --git a/app/utils/cors.py b/app/utils/cors.py new file mode 100644 index 0000000..e7b54e8 --- /dev/null +++ b/app/utils/cors.py @@ -0,0 +1,27 @@ +import os + +from dotenv import load_dotenv +from fastapi.middleware.cors import CORSMiddleware + +load_dotenv() + + +def setup_cors(app): + + dev_origin = os.getenv("FRONTEND_URL", "http://localhost:3000") + prod_origin = os.getenv("PROD_FRONTEND_URL") + + origins = [dev_origin, prod_origin] + + filtered_origins = [] + for origin in origins: + if origin: + filtered_origins.append(origin) + + app.add_middleware( + CORSMiddleware, + allow_origins=filtered_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) |
