diff --git a/apps/api/src/routers/auth.py b/apps/api/src/routers/auth.py index 535ebe74..ff7ea5c6 100644 --- a/apps/api/src/routers/auth.py +++ b/apps/api/src/routers/auth.py @@ -1,11 +1,14 @@ from datetime import timedelta +from typing import Literal, Optional from fastapi import Depends, APIRouter, HTTPException, Response, status, Request from fastapi.security import OAuth2PasswordRequestForm +from pydantic import BaseModel, EmailStr from sqlmodel import Session -from src.db.users import UserRead +from src.db.users import AnonymousUser, PublicUser, UserRead from src.core.events.database import get_db_session from config.config import get_learnhouse_config -from src.security.auth import AuthJWT, authenticate_user +from src.security.auth import AuthJWT, authenticate_user, get_current_user +from src.services.auth.utils import get_google_user_info, signWithGoogle router = APIRouter() @@ -74,6 +77,58 @@ async def login( return result +class ThirdPartyLogin(BaseModel): + email: EmailStr + provider: Literal["google"] + access_token: str + + +@router.post("/oauth") +async def third_party_login( + request: Request, + response: Response, + body: ThirdPartyLogin, + org_id: Optional[int] = None, + current_user: AnonymousUser = Depends(get_current_user), + db_session: Session = Depends(get_db_session), + Authorize: AuthJWT = Depends(), +): + # Google + if body.provider == "google": + + user = await signWithGoogle( + request, body.access_token, body.email, org_id, current_user, db_session + ) + + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect Email or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + access_token = Authorize.create_access_token(subject=user.email) + refresh_token = Authorize.create_refresh_token(subject=user.email) + Authorize.set_refresh_cookies(refresh_token) + + # set cookies using fastapi + response.set_cookie( + key="access_token_cookie", + value=access_token, + httponly=False, + domain=get_learnhouse_config().hosting_config.cookie_config.domain, + expires=int(timedelta(hours=8).total_seconds()), + ) + + user = UserRead.model_validate(user) + + result = { + "user": user, + "tokens": {"access_token": access_token, "refresh_token": refresh_token}, + } + return result + + @router.delete("/logout") def logout(Authorize: AuthJWT = Depends()): """ diff --git a/apps/api/src/services/auth/utils.py b/apps/api/src/services/auth/utils.py new file mode 100644 index 00000000..aa6b0d17 --- /dev/null +++ b/apps/api/src/services/auth/utils.py @@ -0,0 +1,70 @@ +import random +from typing import Optional +from fastapi import Depends, HTTPException, Request +import httpx +from sqlmodel import Session, select +from src.core.events.database import get_db_session +from src.db.users import User, UserCreate, UserRead +from src.security.auth import get_current_user +from src.services.users.users import create_user, create_user_without_org + + +async def get_google_user_info(access_token: str): + url = "https://www.googleapis.com/oauth2/v3/userinfo" + headers = {"Authorization": f"Bearer {access_token}"} + async with httpx.AsyncClient() as client: + response = await client.get(url, headers=headers) + + if response.status_code != 200: + raise HTTPException( + status_code=response.status_code, + detail="Failed to fetch user info from Google", + ) + + return response.json() + + +async def signWithGoogle( + request: Request, + access_token: str, + email: str, + org_id: Optional[int] = None, + current_user=Depends(get_current_user), + db_session: Session = Depends(get_db_session), +): + # Google + google_user = await get_google_user_info(access_token) + + user = db_session.exec( + select(User).where(User.email == google_user["email"]) + ).first() + + if not user: + username = ( + google_user["given_name"] + + google_user["family_name"] + + str(random.randint(10, 99)) + ) + user_object = UserCreate( + email=google_user["email"], + username=username, + password="", + first_name=google_user["given_name"], + last_name=google_user["family_name"], + avatar_image=google_user["picture"], + ) + + if org_id is not None: + user = await create_user( + request, db_session, current_user, user_object, org_id + ) + + return user + else: + user = await create_user_without_org( + request, db_session, current_user, user_object + ) + + return user + + return UserRead.model_validate(user) diff --git a/apps/api/src/services/users/emails.py b/apps/api/src/services/users/emails.py index 95ab0a15..6e20e704 100644 --- a/apps/api/src/services/users/emails.py +++ b/apps/api/src/services/users/emails.py @@ -17,7 +17,7 @@ def send_account_creation_email(
Hello {user.username}
Welcome to LearnHouse! , get started by creating your own organization or join a one.
-Need some help to get started ? LearnHouse Academy
+Need some help to get started ? LearnHouse Academy