From 08cc2bebbe4682ab748417b1483c8e3015c7a595 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Wed, 22 Jul 2026 16:50:11 +0700 Subject: [PATCH] Add Google login functionality and update requirements - Implement Google login endpoint with credential verification - Add GoogleLoginRequest schema for handling login requests - Update Settings class to include GOOGLE_CLIENT_ID - Add google-auth package to requirements --- app/config.py | 1 + app/routers/auth.py | 94 ++++++++++++++++++++++++++++++++++++++++++++- app/schemas/user.py | 4 ++ requirements.txt | 1 + 4 files changed, 99 insertions(+), 1 deletion(-) diff --git a/app/config.py b/app/config.py index 4f645c9..cb797d3 100644 --- a/app/config.py +++ b/app/config.py @@ -13,6 +13,7 @@ class Settings(BaseSettings): SECRET_KEY: str = "change-me-to-a-random-secret-key" ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440 + GOOGLE_CLIENT_ID: str = "" @property def DATABASE_URL(self) -> str: diff --git a/app/routers/auth.py b/app/routers/auth.py index a3eb0ad..5b79c30 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -1,10 +1,23 @@ +import random +import string +from datetime import datetime, timezone + from fastapi import APIRouter, Depends, HTTPException, status +from google.oauth2 import id_token +from google.auth.transport import requests as google_requests from sqlalchemy.orm import Session, selectinload +from app.config import settings from app.database import get_db from app.models.user import User from app.models.user_profile import UserProfile -from app.schemas.user import LoginRequest, LoginResponse, UserCreate, UserResponse +from app.schemas.user import ( + GoogleLoginRequest, + LoginRequest, + LoginResponse, + UserCreate, + UserResponse, +) from app.security import create_access_token, hash_password, verify_password router = APIRouter(prefix="/auth", tags=["auth"]) @@ -70,3 +83,82 @@ def register(data: UserCreate, db: Session = Depends(get_db)): db.refresh(user) return user + + +@router.post("/google-login", response_model=LoginResponse) +def google_login(data: GoogleLoginRequest, db: Session = Depends(get_db)): + try: + idinfo = id_token.verify_oauth2_token( + data.credential, + google_requests.Request(), + settings.GOOGLE_CLIENT_ID, + ) + except Exception: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail={"message": "Kredensial Google tidak valid atau telah kedaluwarsa."}, + ) + + email = idinfo.get("email") + name = idinfo.get("name", "") + picture = idinfo.get("picture", "") + + if not email: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail={"message": "Tidak dapat mengambil email dari akun Google."}, + ) + + user = db.query(User).options(selectinload(User.profile)).filter( + User.email == email, + User.deleted_at.is_(None), + ).first() + + if not user: + base_username = email.split("@")[0] + username = base_username + suffix = 1 + while db.query(User).filter(User.username == username).first(): + suffix += 1 + username = f"{base_username}{suffix}" + + random_password = "".join(random.choices(string.ascii_letters + string.digits, k=32)) + + user = User( + email=email, + username=username, + password=hash_password(random_password), + email_verified_at=datetime.now(timezone.utc), + status="ACTIVE", + ) + db.add(user) + db.flush() + + profile = UserProfile( + user_id=user.id, + full_name=name, + timezone="Asia/Jakarta", + ) + db.add(profile) + db.commit() + db.refresh(user) + else: + if user.email_verified_at is None: + user.email_verified_at = datetime.now(timezone.utc) + if not user.profile: + profile = UserProfile( + user_id=user.id, + full_name=name, + timezone="Asia/Jakarta", + ) + db.add(profile) + elif user.profile.full_name != name: + user.profile.full_name = name + db.commit() + db.refresh(user) + + access_token = create_access_token({"sub": user.id}) + return LoginResponse( + access_token=access_token, + user=user, + ) diff --git a/app/schemas/user.py b/app/schemas/user.py index dc57538..7bced86 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -56,3 +56,7 @@ class LoginResponse(BaseModel): access_token: str token_type: str = "bearer" user: UserDetailResponse + + +class GoogleLoginRequest(BaseModel): + credential: str diff --git a/requirements.txt b/requirements.txt index eda4154..eadfe3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ dnspython==2.8.0 email-validator==2.3.0 exceptiongroup==1.3.1 fastapi==0.139.0 +google-auth==2.56.2 fastapi-cli==0.0.29 fastapi-cloud-cli==0.22.1 fastar==0.11.0