Merge pull request #260 from learnhouse/feat/use-next-auth

New Auth Mechanism + Google OAuth
This commit is contained in:
Badr B 2024-06-07 15:12:21 +01:00 committed by GitHub
commit ddbd413539
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
140 changed files with 2833 additions and 1919 deletions

View file

@ -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, 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 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()):
"""

View file

@ -8,6 +8,7 @@ from src.services.orgs.invites import (
get_invite_code,
get_invite_codes,
)
from src.services.orgs.join import JoinOrg, join_org
from src.services.orgs.users import (
get_list_of_invited_users,
get_organization_users,
@ -99,6 +100,19 @@ async def api_get_org_users(
return await get_organization_users(request, org_id, db_session, current_user)
@router.post("/join")
async def api_join_an_org(
request: Request,
args: JoinOrg,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
):
"""
Get single Org by ID
"""
return await join_org(request, args, current_user, db_session)
@router.put("/{org_id}/users/{user_id}/role/{role_uuid}")
async def api_update_user_role(
request: Request,

View file

@ -85,7 +85,6 @@ async def api_create_user_with_orgid(
"""
Create User with Org ID
"""
print(await get_org_join_mechanism(request, org_id, current_user, db_session))
# TODO(fix) : This is temporary, logic should be moved to service
if (

View file

@ -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)

View file

@ -0,0 +1,119 @@
from datetime import datetime
from typing import Optional
from fastapi import HTTPException, Request
from pydantic import BaseModel
from sqlmodel import Session, select
from src.db.organizations import Organization
from src.db.user_organizations import UserOrganization
from src.db.users import AnonymousUser, PublicUser, User
from src.services.orgs.invites import get_invite_code
from src.services.orgs.orgs import get_org_join_mechanism
class JoinOrg(BaseModel):
org_id: int
user_id: str
invite_code: Optional[str]
async def join_org(
request: Request,
args: JoinOrg,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Organization).where(Organization.id == args.org_id)
result = db_session.exec(statement)
org = result.first()
if not org:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
join_method = await get_org_join_mechanism(
request, args.org_id, current_user, db_session
)
# Get User
statement = select(User).where(User.id == args.user_id)
result = db_session.exec(statement)
user = result.first()
# Check if User isn't already part of the org
statement = select(UserOrganization).where(
UserOrganization.user_id == args.user_id, UserOrganization.org_id == args.org_id
)
result = db_session.exec(statement)
userorg = result.first()
if userorg:
raise HTTPException(
status_code=400, detail="User is already part of that organization"
)
if join_method == "inviteOnly" and user and org and args.invite_code:
if user.id is not None and org.id is not None:
# Check if invite code exists
inviteCode = await get_invite_code(
request, org.id, args.invite_code, current_user, db_session
)
if not inviteCode:
raise HTTPException(
status_code=400,
detail="Invite code is incorrect",
)
# Link user and organization
user_organization = UserOrganization(
user_id=user.id,
org_id=org.id,
role_id=3,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
db_session.add(user_organization)
db_session.commit()
return "Great, You're part of the Organization"
else:
raise HTTPException(
status_code=403,
detail="Something wrong, try later.",
)
if join_method == "open" and user and org:
if user.id is not None and org.id is not None:
# Link user and organization
user_organization = UserOrganization(
user_id=user.id,
org_id=org.id,
role_id=3,
creation_date=str(datetime.now()),
update_date=str(datetime.now()),
)
db_session.add(user_organization)
db_session.commit()
return "Great, You're part of the Organization"
else:
raise HTTPException(
status_code=403,
detail="Something wrong, try later.",
)
else:
raise HTTPException(
status_code=403,
detail="Something wrong, try later.",
)

View file

@ -436,8 +436,7 @@ async def get_orgs_by_user(
orgs = result.all()
return orgs
return orgs #type:ignore
# Config related
async def update_org_signup_mechanism(

View file

@ -17,7 +17,7 @@ def send_account_creation_email(
<body>
<p>Hello {user.username}</p>
<p>Welcome to LearnHouse! , get started by creating your own organization or join a one.</p>
<p>Need some help to get started ? <a href="https://learn.learnhouse.io">LearnHouse Academy</a></p>
<p>Need some help to get started ? <a href="https://university.learnhouse.io">LearnHouse Academy</a></p>
</body>
</html>
""",