hisham-cmd's picture
Full Sync: Explicit Commit Operations (Pure & Optimized)
b2be963 verified
Raw
History Blame Contribute Delete
9.28 kB
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from google.oauth2 import id_token
from google.auth.transport import requests as google_requests
from app.db.base import get_db
from app.models.user import User, AuthProvider, UserRole
from app.schemas.auth import (
RegisterRequest,
LoginRequest,
GoogleLoginRequest,
RefreshRequest,
TokenResponse,
UserResponse,
AuthResponse,
)
from app.core.security import (
hash_password,
verify_password,
create_access_token,
create_refresh_token,
decode_token,
get_current_user,
get_optional_current_user,
)
from app.core.config import settings
router = APIRouter(prefix="/auth", tags=["Authentication"])
def _generate_tokens(user_id: int) -> dict:
access_token = create_access_token({"sub": str(user_id)})
refresh_token = create_refresh_token({"sub": str(user_id)})
return {
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "bearer",
}
@router.post("/register", response_model=AuthResponse, status_code=status.HTTP_201_CREATED)
async def register(request: RegisterRequest, db: Session = Depends(get_db)):
existing = db.query(User).filter(User.email == request.email).first()
if existing:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Email already registered",
)
user = User(
name=request.name,
email=request.email,
password_hash=hash_password(request.password),
phone=request.phone,
role=UserRole.CUSTOMER,
auth_provider=AuthProvider.LOCAL,
)
db.add(user)
db.commit()
db.refresh(user)
tokens = _generate_tokens(user.id)
user_data = UserResponse.model_validate(user).model_dump()
user_data["created_at"] = user_data["created_at"].isoformat()
return AuthResponse(
isSuccess=True,
value={"tokens": tokens, "user": user_data},
statusCode=201,
)
@router.post("/login", response_model=AuthResponse)
async def login(request: LoginRequest, db: Session = Depends(get_db)):
user = db.query(User).filter(User.email == request.email).first()
if not user or not user.password_hash:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password",
)
if not verify_password(request.password, user.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password",
)
tokens = _generate_tokens(user.id)
user_data = UserResponse.model_validate(user).model_dump()
user_data["created_at"] = user_data["created_at"].isoformat()
return AuthResponse(
isSuccess=True,
value={"tokens": tokens, "user": user_data},
statusCode=200,
)
@router.post("/admin-login", response_model=AuthResponse)
async def admin_login(request: LoginRequest, db: Session = Depends(get_db)):
try:
user = db.query(User).filter(User.email == request.email).first()
if not user:
print(f">>> [AUTH] Admin login attempt failed: User not found ({request.email})")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password",
)
if not user.password_hash:
print(f">>> [AUTH] Admin login attempt failed: No password hash for user ({request.email})")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password",
)
if not verify_password(request.password, user.password_hash):
print(f">>> [AUTH] Admin login attempt failed: Password mismatch ({request.email})")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password",
)
# Crucial step: Verify admin role
if user.role != UserRole.ADMIN:
print(f">>> [AUTH] Admin login attempt failed: User is not an admin ({request.email}, role: {user.role})")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied. Admin privileges required.",
)
print(f">>> [AUTH] Admin login successful: {request.email}")
tokens = _generate_tokens(user.id)
user_data = UserResponse.model_validate(user).model_dump()
user_data["created_at"] = user_data["created_at"].isoformat()
return AuthResponse(
isSuccess=True,
value={"tokens": tokens, "user": user_data},
statusCode=200,
)
except HTTPException:
raise
except Exception as e:
import traceback
traceback.print_exc()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Login error: {str(e)}",
)
@router.post("/google", response_model=AuthResponse)
async def google_login(request: GoogleLoginRequest, db: Session = Depends(get_db)):
"""Authenticate user with Google ID token."""
if not settings.GOOGLE_CLIENT_ID:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Google authentication is not configured",
)
try:
idinfo = id_token.verify_oauth2_token(
request.credential,
google_requests.Request(),
settings.GOOGLE_CLIENT_ID,
)
except ValueError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid Google token",
)
google_id = idinfo.get("sub")
email = idinfo.get("email")
name = idinfo.get("name", "")
avatar_url = idinfo.get("picture", "")
if not email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Google account does not have an email",
)
# Check if user exists by google_id
user = db.query(User).filter(User.google_id == google_id).first()
if not user:
# Check if user exists by email (link accounts)
user = db.query(User).filter(User.email == email).first()
if user:
# Link existing account to Google
user.google_id = google_id
user.auth_provider = AuthProvider.GOOGLE
if not user.avatar_url:
user.avatar_url = avatar_url
else:
# Create new user
user = User(
name=name,
email=email,
google_id=google_id,
avatar_url=avatar_url,
auth_provider=AuthProvider.GOOGLE,
role=UserRole.CUSTOMER,
)
db.add(user)
db.commit()
db.refresh(user)
tokens = _generate_tokens(user.id)
user_data = UserResponse.model_validate(user).model_dump()
user_data["created_at"] = user_data["created_at"].isoformat()
return AuthResponse(
isSuccess=True,
value={"tokens": tokens, "user": user_data},
statusCode=200,
)
@router.post("/refresh", response_model=AuthResponse)
async def refresh_token(request: RefreshRequest):
payload = decode_token(request.refresh_token)
if payload.get("type") != "refresh":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token type — expected refresh token",
)
user_id = payload.get("sub")
tokens = _generate_tokens(int(user_id))
return AuthResponse(
isSuccess=True,
value={"tokens": tokens},
statusCode=200,
)
@router.get("/me", response_model=AuthResponse)
async def get_me(
user_id: int = Depends(get_current_user),
db: Session = Depends(get_db),
):
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
user_data = UserResponse.model_validate(user).model_dump()
user_data["created_at"] = user_data["created_at"].isoformat()
return AuthResponse(
isSuccess=True,
value={"user": user_data},
statusCode=200,
)
async def get_current_admin_user(
user_id: int = Depends(get_current_user),
db: Session = Depends(get_db),
) -> User:
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
if user.role != UserRole.ADMIN:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin privileges required",
)
return user