165 lines
5.2 KiB
Python
165 lines
5.2 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
|
|
from fastapi.security import HTTPBearer
|
|
from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase
|
|
|
|
from ...core.config import settings
|
|
from ...core.database import get_database
|
|
from ...core.security import (
|
|
create_access_token,
|
|
create_refresh_token,
|
|
decode_token,
|
|
verify_password,
|
|
)
|
|
from ...models.user import User
|
|
from ...schemas.auth import LoginRequest, LoginResponse, LogoutResponse, RefreshResponse
|
|
|
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
security = HTTPBearer()
|
|
|
|
|
|
@router.post("/login", response_model=LoginResponse)
|
|
async def login(
|
|
login_data: LoginRequest,
|
|
response: Response,
|
|
):
|
|
print(f"LOGIN: Starting login for {login_data.email}")
|
|
# Create database connection directly (bypass dependency injection issues)
|
|
client = AsyncIOMotorClient(settings.mongodb_uri)
|
|
db = client[settings.mongodb_db]
|
|
|
|
try:
|
|
print("LOGIN: Database connection created")
|
|
# Find user by email
|
|
print("LOGIN: Looking up user in database")
|
|
user_doc = await db.users.find_one({"email": login_data.email})
|
|
print(f"LOGIN: User lookup complete, found: {user_doc is not None}")
|
|
if not user_doc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Incorrect email or password",
|
|
)
|
|
|
|
user = User(**user_doc)
|
|
|
|
# Verify password
|
|
if not verify_password(login_data.password, user.hashed_password):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Incorrect email or password",
|
|
)
|
|
|
|
if not user.is_active:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="User account is disabled",
|
|
)
|
|
|
|
# Create tokens
|
|
access_token = create_access_token(subject=str(user.id))
|
|
refresh_token = create_refresh_token(subject=str(user.id))
|
|
|
|
# Set refresh token as HttpOnly cookie
|
|
response.set_cookie(
|
|
key="refresh_token",
|
|
value=refresh_token,
|
|
httponly=True,
|
|
secure=settings.cookie_secure,
|
|
samesite=settings.cookie_samesite,
|
|
domain=settings.cookie_domain if settings.app_env == "prod" else None,
|
|
max_age=settings.jwt_refresh_ttl_days * 24 * 60 * 60,
|
|
)
|
|
|
|
return LoginResponse(
|
|
access_token=access_token,
|
|
user_id=str(user.id),
|
|
role=user.role,
|
|
)
|
|
|
|
finally:
|
|
# Close database connection
|
|
client.close()
|
|
|
|
|
|
@router.post("/refresh", response_model=RefreshResponse)
|
|
async def refresh_token(
|
|
request: Request,
|
|
response: Response,
|
|
db: AsyncIOMotorDatabase = Depends(get_database),
|
|
):
|
|
refresh_token = request.cookies.get("refresh_token")
|
|
if not refresh_token:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Refresh token not found",
|
|
)
|
|
|
|
try:
|
|
payload = decode_token(refresh_token)
|
|
if payload.get("type") != "refresh":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid token type",
|
|
)
|
|
|
|
user_id = payload.get("sub")
|
|
if not user_id:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid token",
|
|
)
|
|
|
|
# Verify user still exists and is active
|
|
user_doc = await db.users.find_one({"_id": user_id})
|
|
if not user_doc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="User not found",
|
|
)
|
|
|
|
user = User(**user_doc)
|
|
if not user.is_active:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="User account is disabled",
|
|
)
|
|
|
|
# Create new tokens
|
|
new_access_token = create_access_token(subject=user_id)
|
|
new_refresh_token = create_refresh_token(subject=user_id)
|
|
|
|
# Update refresh token cookie
|
|
response.set_cookie(
|
|
key="refresh_token",
|
|
value=new_refresh_token,
|
|
httponly=True,
|
|
secure=settings.cookie_secure,
|
|
samesite=settings.cookie_samesite,
|
|
domain=settings.cookie_domain if settings.app_env == "prod" else None,
|
|
max_age=settings.jwt_refresh_ttl_days * 24 * 60 * 60,
|
|
)
|
|
|
|
return RefreshResponse(
|
|
access_token=new_access_token,
|
|
user_id=user_id,
|
|
role=user.role.value
|
|
)
|
|
|
|
except Exception:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid refresh token",
|
|
)
|
|
|
|
|
|
@router.post("/logout", response_model=LogoutResponse)
|
|
async def logout(response: Response):
|
|
# Clear refresh token cookie
|
|
response.delete_cookie(
|
|
key="refresh_token",
|
|
httponly=True,
|
|
secure=settings.cookie_secure,
|
|
samesite=settings.cookie_samesite,
|
|
domain=settings.cookie_domain if settings.app_env == "prod" else None,
|
|
)
|
|
|
|
return LogoutResponse()
|