Files
rengine/backend/app/api/v1/auth.py
T
2026-01-26 15:02:53 +05:30

213 lines
6.1 KiB
Python

from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select
from app.api.deps import CurrentSuperuser, CurrentUser
from app.config import settings
from app.core.database import get_session
from app.core.security import (
create_access_token,
create_refresh_token,
decode_token,
hash_password,
verify_password,
)
from app.models.user import User, UserCreate, UserRead
from app.schemas.auth import LoginRequest, TokenResponse
router = APIRouter(prefix="/auth", tags=["authentication"])
def set_auth_cookies(
response: Response,
access_token: str,
refresh_token: str,
) -> None:
response.set_cookie(
key="access_token",
value=access_token,
httponly=True,
secure=not settings.DEBUG,
samesite="lax",
max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
path="/",
)
response.set_cookie(
key="refresh_token",
value=refresh_token,
httponly=True,
secure=not settings.DEBUG,
samesite="lax",
max_age=settings.REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60,
path="/",
)
def clear_auth_cookies(response: Response) -> None:
response.delete_cookie(key="access_token", path="/")
response.delete_cookie(key="refresh_token", path="/")
@router.post("/login", response_model=TokenResponse)
async def login(
login_data: LoginRequest,
response: Response,
session: Annotated[AsyncSession, Depends(get_session)],
):
result = await session.execute(
select(User).where(User.username == login_data.username)
)
user = result.scalar_one_or_none()
if not user or not verify_password(login_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User account is inactive",
)
access_token = create_access_token(str(user.id))
refresh_token = create_refresh_token(str(user.id))
set_auth_cookies(response, access_token, refresh_token)
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token,
)
@router.post("/refresh", response_model=TokenResponse)
async def refresh_access_token(
request: Request,
response: Response,
session: Annotated[AsyncSession, Depends(get_session)],
):
refresh_token = request.cookies.get("refresh_token")
if not refresh_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Refresh token not found",
headers={"WWW-Authenticate": "Bearer"},
)
payload = decode_token(refresh_token)
if payload is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired refresh token",
headers={"WWW-Authenticate": "Bearer"},
)
if payload.get("type") != "refresh":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token type",
headers={"WWW-Authenticate": "Bearer"},
)
user_id_str = payload.get("sub")
if not user_id_str:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token payload",
headers={"WWW-Authenticate": "Bearer"},
)
try:
user_id = UUID(user_id_str)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid user ID in token",
headers={"WWW-Authenticate": "Bearer"},
) from e
result = await session.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User account is inactive",
)
new_access_token = create_access_token(str(user.id))
new_refresh_token = create_refresh_token(str(user.id))
set_auth_cookies(response, new_access_token, new_refresh_token)
return TokenResponse(
access_token=new_access_token,
refresh_token=new_refresh_token,
)
@router.post("/logout")
async def logout(response: Response):
clear_auth_cookies(response)
return {"message": "Successfully logged out"}
# lets add protected routers here
# get logged in user info
@router.get("/me", response_model=UserRead)
async def get_current_user_info(current_user: CurrentUser):
return current_user
@router.post("/register", response_model=UserRead, status_code=status.HTTP_201_CREATED)
async def register_user(
user_in: UserCreate,
session: Annotated[AsyncSession, Depends(get_session)],
current_user: CurrentSuperuser, # noqa: ARG001
):
"""
Register a new user. **Admin only**.
"""
result = await session.execute(select(User).where(User.email == user_in.email))
if result.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User with this email already exists",
)
result = await session.execute(
select(User).where(User.username == user_in.username)
)
if result.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User with this username already exists",
)
user = User(
email=user_in.email,
username=user_in.username,
hashed_password=hash_password(user_in.password),
)
session.add(user)
await session.commit()
await session.refresh(user)
return user