summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/auth/jwt.py2
-rw-r--r--app/main.py2
-rw-r--r--app/models/user.py3
-rw-r--r--app/routes/auth.py37
-rw-r--r--app/routes/me.py54
-rw-r--r--app/routes/register.py93
-rw-r--r--app/utils/cors.py27
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=["*"],
+ )