mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
Merge pull request #260 from learnhouse/feat/use-next-auth
New Auth Mechanism + Google OAuth
This commit is contained in:
commit
ddbd413539
140 changed files with 2833 additions and 1919 deletions
|
|
@ -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()):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
70
apps/api/src/services/auth/utils.py
Normal file
70
apps/api/src/services/auth/utils.py
Normal 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)
|
||||
119
apps/api/src/services/orgs/join.py
Normal file
119
apps/api/src/services/orgs/join.py
Normal 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.",
|
||||
)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
""",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue