summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/auth/jwt.py71
-rw-r--r--app/auth/login.py0
-rw-r--r--app/auth/register.py32
-rw-r--r--app/main.py21
-rw-r--r--app/models/models.py0
-rw-r--r--app/models/user.py26
-rw-r--r--app/routes/__init__.py12
-rw-r--r--app/routes/auth.py37
-rw-r--r--app/routes/me.py48
-rw-r--r--app/routes/register.py2
-rw-r--r--app/routes/routes.py6
-rw-r--r--app/schemas/schemas.py0
-rw-r--r--app/schemas/user.py4
-rw-r--r--app/utils/create_tables.py1
-rw-r--r--app/utils/db.py3
15 files changed, 199 insertions, 64 deletions
diff --git a/app/auth/jwt.py b/app/auth/jwt.py
index e69de29..2d2aac5 100644
--- a/app/auth/jwt.py
+++ b/app/auth/jwt.py
@@ -0,0 +1,71 @@
+import os
+from datetime import datetime, timedelta
+from typing import Dict, Optional
+
+import jwt
+from dotenv import load_dotenv
+
+from app.utils.logger_cfg import logger
+
+load_dotenv()
+JWT_SECRET = os.getenv("JWT_SECRET")
+if not JWT_SECRET:
+ logger.critical("JWT_SECRET environment variable not set! Exiting.")
+ raise RuntimeError("JWT_SECRET environment variable not set!")
+
+JWT_ALGORITHM = "HS256"
+ACCESS_TOKEN_EXPIRE_MINUTES = 60
+REFRESH_TOKEN_EXPIRE_DAYS = 7
+
+
+def create_access_token(data: Dict, expires_delta: Optional[timedelta] = None) -> str:
+ try:
+ to_encode = data.copy()
+ expire = datetime.utcnow() + (
+ expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
+ )
+ to_encode.update({"exp": expire})
+ safe_payload = {k: v for k, v in to_encode.items() if k != "password"}
+ logger.debug(f"Creating access token with payload: {safe_payload}")
+ return jwt.encode(to_encode, JWT_SECRET, algorithm=JWT_ALGORITHM)
+ except Exception as e:
+ logger.exception(f"Failed to create access token: {e}")
+ raise RuntimeError("Failed to create access token")
+
+
+def create_refresh_token(data: Dict, expires_delta: Optional[timedelta] = None) -> str:
+ try:
+ to_encode = data.copy()
+ expire = datetime.utcnow() + (
+ expires_delta or timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
+ )
+ to_encode.update({"exp": expire})
+ logger.debug(f"Creating refresh token with payload: {to_encode}")
+ return jwt.encode(to_encode, JWT_SECRET, algorithm=JWT_ALGORITHM)
+ except Exception as e:
+ logger.exception(f"Failed to create refresh token: {e}")
+ raise RuntimeError("Failed to create refresh token")
+
+
+def decode_token(token: str) -> Dict:
+ try:
+ logger.debug("Decoding JWT token...")
+ payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
+ safe_payload = {k: v for k, v in payload.items() if k != "password"}
+ logger.info(f"JWT decoded successfully: {safe_payload}")
+ return payload
+ except jwt.ExpiredSignatureError:
+ logger.warning("JWT token has expired")
+ raise ValueError("Token has expired")
+ except jwt.InvalidSignatureError:
+ logger.warning("JWT token signature invalid")
+ raise ValueError("Invalid token signature")
+ except jwt.DecodeError:
+ logger.warning("JWT token decode failed (possibly malformed)")
+ raise ValueError("Malformed token")
+ except jwt.InvalidTokenError:
+ logger.warning("JWT token invalid for unknown reason")
+ raise ValueError("Invalid token")
+ except Exception as e:
+ logger.exception(f"Unexpected error decoding JWT: {e}")
+ raise RuntimeError("Unexpected error while decoding token")
diff --git a/app/auth/login.py b/app/auth/login.py
deleted file mode 100644
index e69de29..0000000
--- a/app/auth/login.py
+++ /dev/null
diff --git a/app/auth/register.py b/app/auth/register.py
deleted file mode 100644
index f1c3ec3..0000000
--- a/app/auth/register.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from app.models.auth import User
-from argon2 import PasswordHasher
-from fastapi import APIRouter, Depends, HTTPException, status
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy.future import select
-
-from app.schemas.auth import RegisterSchema
-from app.utils.db import get_db
-
-router = APIRouter()
-ph = PasswordHasher()
-
-
-@router.post("/register")
-async def register(user: RegisterSchema, db: AsyncSession = Depends(get_db)):
- result = await db.execute(
- select(User).where(
- (User.username == user.username) | (User.email == user.email)
- )
- )
- existing = result.scalar_one_or_none()
- if existing:
- raise HTTPException(
- status_code=status.HTTP_400_BAD_REQUEST,
- detail="Username or email already exists",
- )
-
- hashed = ph.hash(user.password)
- new_user = User(username=user.username, email=user.email, password_hash=hashed)
- db.add(new_user)
- await db.commit()
- return {"message": "User registered successfully"}
diff --git a/app/main.py b/app/main.py
index 8b7d6eb..d2925ed 100644
--- a/app/main.py
+++ b/app/main.py
@@ -3,7 +3,7 @@ import time
from fastapi import FastAPI
from sqlalchemy import text
-from app.routes.routes import router as api_router
+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
@@ -65,22 +65,3 @@ async def read_root():
logger.info("Root endpoint accessed")
logger.debug("Processing root endpoint request")
return {"message": "Hello new asyncpg ci!"}
-
-
-@app.get("/check-db")
-async def check_db():
- logger.info("Database health check endpoint called")
- logger.debug("Starting database health check")
-
- try:
- async with engine.begin() as conn:
- logger.debug("Executing test query: SELECT 1")
- result = await conn.execute(text("SELECT 1"))
- db_result = result.scalar()
- logger.info(f"Database health check successful: {db_result}")
- logger.debug("Database health check query executed successfully")
- return {"db_check": db_result}
-
- except Exception as e:
- logger.exception(f"Database health check failed: {e}")
- raise
diff --git a/app/models/models.py b/app/models/models.py
deleted file mode 100644
index e69de29..0000000
--- a/app/models/models.py
+++ /dev/null
diff --git a/app/models/user.py b/app/models/user.py
index de9fa98..76ba3f9 100644
--- a/app/models/user.py
+++ b/app/models/user.py
@@ -1,6 +1,11 @@
+from fastapi import Depends
from sqlalchemy import Boolean, Integer, String
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy.future import select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
+from app.utils.db import get_async_session
+
class Base(DeclarativeBase):
pass
@@ -21,3 +26,24 @@ 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)
+
+ @classmethod
+ async def get_user_by_email(
+ cls, email: str, session: AsyncSession = Depends(get_async_session)
+ ):
+ result = await session.execute(select(cls).where(cls.email == email))
+ return result.scalars().first()
+
+ @classmethod
+ async def get_user_by_username(
+ cls, username: str, session: AsyncSession = Depends(get_async_session)
+ ):
+ result = await session.execute(select(cls).where(cls.username == username))
+ return result.scalars().first()
+
+ @classmethod
+ async def get_user_by_id(
+ cls, user_id: int, session: AsyncSession = Depends(get_async_session)
+ ):
+ result = await session.execute(select(cls).where(cls.id == user_id))
+ return result.scalars().first()
diff --git a/app/routes/__init__.py b/app/routes/__init__.py
index e69de29..383ef66 100644
--- a/app/routes/__init__.py
+++ b/app/routes/__init__.py
@@ -0,0 +1,12 @@
+from fastapi import APIRouter
+
+from .auth import router as auth_router
+from .me import router as me_router
+from .register import router as register_router
+
+router = APIRouter()
+
+router.include_router(register_router, prefix="/auth")
+router.include_router(auth_router, prefix="/auth")
+
+router.include_router(me_router)
diff --git a/app/routes/auth.py b/app/routes/auth.py
new file mode 100644
index 0000000..aa68c52
--- /dev/null
+++ b/app/routes/auth.py
@@ -0,0 +1,37 @@
+from fastapi import APIRouter, Depends, HTTPException
+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
+
+router = APIRouter(tags=["auth"])
+
+
+@router.post("/login")
+async def login(
+ form_data: OAuth2PasswordRequestForm = Depends(),
+ session: AsyncSession = Depends(get_async_session),
+):
+ user = await User.get_user_by_email(form_data.username, session=session)
+ if not user:
+ user = await User.get_user_by_username(form_data.username, session=session)
+
+ if not user or not verify_password(form_data.password, user.password):
+ 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)})
+
+ logger.info("User logged in | id={} username={}", user.id, user.username)
+
+ return {
+ "access_token": access_token,
+ "refresh_token": refresh_token,
+ "token_type": "bearer",
+ }
diff --git a/app/routes/me.py b/app/routes/me.py
new file mode 100644
index 0000000..a09453c
--- /dev/null
+++ b/app/routes/me.py
@@ -0,0 +1,48 @@
+from fastapi import APIRouter, Depends, HTTPException
+from fastapi.security import OAuth2PasswordBearer
+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.get("/me")
+async def read_current_user(
+ token: str = Depends(oauth2_scheme),
+ session: AsyncSession = Depends(get_async_session),
+):
+
+ 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)
+
+ if not user:
+ logger.warning("User not found in /me | id={}", user_id)
+ raise HTTPException(status_code=404, detail="User not found")
+
+ logger.info("User accessed /me | id={} username={}", user.id, user.username)
+
+ 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")
diff --git a/app/routes/register.py b/app/routes/register.py
index 0134c56..fb8ec3d 100644
--- a/app/routes/register.py
+++ b/app/routes/register.py
@@ -11,7 +11,7 @@ from app.utils.db import get_async_session
from app.utils.hash_cfg import hash_password
from app.utils.logger_cfg import logger
-router = APIRouter(prefix="/auth", tags=["auth"])
+router = APIRouter(tags=["auth"])
@router.post("/register", response_model=UserRead)
diff --git a/app/routes/routes.py b/app/routes/routes.py
deleted file mode 100644
index 7db7ec4..0000000
--- a/app/routes/routes.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from fastapi import APIRouter
-
-from .register import router as register_router
-
-router = APIRouter()
-router.include_router(register_router)
diff --git a/app/schemas/schemas.py b/app/schemas/schemas.py
deleted file mode 100644
index e69de29..0000000
--- a/app/schemas/schemas.py
+++ /dev/null
diff --git a/app/schemas/user.py b/app/schemas/user.py
index 7151d54..6644216 100644
--- a/app/schemas/user.py
+++ b/app/schemas/user.py
@@ -5,7 +5,7 @@ from pydantic import BaseModel, EmailStr, Field
class UserCreate(BaseModel):
username: str = Field(..., max_length=32)
- email: Optional[EmailStr] = None
+ email: EmailStr
password: str = Field(..., min_length=8)
model_config = {
@@ -16,7 +16,7 @@ class UserCreate(BaseModel):
class UserRead(BaseModel):
id: int
username: str
- email: Optional[EmailStr] = None
+ email: EmailStr = None
google_id: Optional[str] = None
avatar_file: Optional[str] = None
banner_file: Optional[str] = None
diff --git a/app/utils/create_tables.py b/app/utils/create_tables.py
index bb9baf3..e438a03 100644
--- a/app/utils/create_tables.py
+++ b/app/utils/create_tables.py
@@ -1,4 +1,3 @@
-# app/utils/create_tables.py
from app.models.user import Base
from app.utils.db import engine
diff --git a/app/utils/db.py b/app/utils/db.py
index 603ff68..b823904 100644
--- a/app/utils/db.py
+++ b/app/utils/db.py
@@ -1,4 +1,3 @@
-# app/utils/db.py
import os
from dotenv import load_dotenv
@@ -9,7 +8,7 @@ load_dotenv()
DATABASE_URL = os.getenv("DATABASE_URL")
if not DATABASE_URL:
- raise ValueError("DATABASE_URL не найден в .env!")
+ raise ValueError("DATABASE_URL not found .env")
engine = create_async_engine(DATABASE_URL, echo=True, future=True)