Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 14 additions & 75 deletions app/api/routes/auth.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,22 @@
from datetime import datetime

from authlib.integrations.starlette_client import OAuth, OAuthError
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import JSONResponse
from fastapi import APIRouter, Depends, Request
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from starlette.config import Config

from app.config.database import get_db
from app.models.user import AuthType, User
from app.settings import settings
from app.utils import auth
from app.config.github import github
from app.schemas.users import Token
from app.services.auth import login_github_user, login_user

router = APIRouter()

# TODO: Move this to a separate config file
oauth = OAuth(
Config(
environ={
"GITHUB_CLIENT_ID": settings.GITHUB_CLIENT_ID,
"GITHUB_CLIENT_SECRET": settings.GITHUB_CLIENT_SECRET,
}
)
)
github = oauth.register(
name="github",
client_id=settings.GITHUB_CLIENT_ID,
client_secret=settings.GITHUB_CLIENT_SECRET,
access_token_url="https://github.com/login/oauth/access_token",
access_token_params=None,
authorize_url="https://github.com/login/oauth/authorize",
authorize_params=None,
api_base_url="https://api.github.com/",
client_kwargs={"scope": "user:email"},
)

# Standard username/password login route
@router.post("/login", response_model=Token)
def login(
form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)
):
"""Simple login route using username and password."""
return login_user(db, form_data.username, form_data.password)


@router.get("/github/login")
Expand All @@ -45,49 +29,4 @@ async def login_via_github(request: Request):
@router.get("/github/callback")
async def auth_github_callback(request: Request, db: Session = Depends(get_db)):
"""Handles the GitHub OAuth callback and logs in or registers the user."""
try:
token = await github.authorize_access_token(request)
resp = await github.get("user", token=token)
profile = resp.json()
email = profile.get("email")
name = profile.get("name") or profile.get("login")
provider_id = str(profile.get("id"))

# If email is not public, fetch from emails endpoint
if not email:
emails_resp = await github.get("user/emails", token=token)
emails = emails_resp.json()
email = next(
(e["email"] for e in emails if e.get("primary") and e.get("verified")),
None,
)
if not email:
raise HTTPException(
status_code=400, detail="GitHub account has no accessible email."
)

user = db.query(User).filter(User.email == email).first()
if not user:
# TODO: Use existing create user logic
user = User(
email=email,
name=name,
provider_id=provider_id,
auth_type=AuthType.GITHUB,
is_active=True,
created_at=datetime.utcnow(),
)
db.add(user)
db.commit()
db.refresh(user)

access_token = auth.create_access_token(
{"sub": str(user.id), "email": user.email}
)
return JSONResponse({"access_token": access_token, "token_type": "bearer"})
except OAuthError as e:
# Log the detailed exception for debugging purposes
import logging
logging.error("OAuthError occurred during GitHub authentication", exc_info=e)
# Return a generic error message to the client
return JSONResponse({"error": "An error occurred during authentication. Please try again later."}, status_code=400)
return await login_github_user(request, db)
18 changes: 8 additions & 10 deletions app/api/routes/user.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,31 @@
from fastapi import APIRouter, Depends
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session

from app.config.database import get_db
from app.schemas.users import OTPVerifyRequest, Token, UserCreate, UserResponse
from app.services.user import (get_current_user, login_user, register_with_otp,
from app.schemas.users import OTPVerifyRequest, UserCreate, UserResponse
from app.services.user import (get_current_user, register_with_otp,
verify_otp_and_create_user)

router = APIRouter()


@router.post("/register", status_code=201)
def register_user(user_in: UserCreate, db: Session = Depends(get_db)):
"""Register a new user with OTP verification.
It initiates the OTP process and returns a response to the user.
The user must then verify the OTP to complete registration."""
return register_with_otp(db, user_in)


@router.post("/verify-otp", response_model=UserResponse)
def verify_otp(request: OTPVerifyRequest, db: Session = Depends(get_db)):
"""Verify OTP and create user.
This endpoint is called after the user has received an OTP via email.
It checks the OTP and creates the user if the OTP is valid."""
user = verify_otp_and_create_user(db, request.email, request.otp_code)
return user


@router.post("/login", response_model=Token)
def login(
form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)
):
return login_user(db, form_data.username, form_data.password)


@router.get("/me")
def read_users_me(current_user=Depends(get_current_user)):
if hasattr(current_user, "id"):
Expand Down
25 changes: 25 additions & 0 deletions app/config/github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from authlib.integrations.starlette_client import OAuth
from starlette.config import Config

from app.settings import settings

oauth = OAuth(
Config(
environ={
"GITHUB_CLIENT_ID": settings.GITHUB_CLIENT_ID,
"GITHUB_CLIENT_SECRET": settings.GITHUB_CLIENT_SECRET,
}
)
)

github = oauth.register(
name="github",
client_id=settings.GITHUB_CLIENT_ID,
client_secret=settings.GITHUB_CLIENT_SECRET,
access_token_url="https://github.com/login/oauth/access_token",
access_token_params=None,
authorize_url="https://github.com/login/oauth/authorize",
authorize_params=None,
api_base_url="https://api.github.com/",
client_kwargs={"scope": "user:email"},
)
1 change: 1 addition & 0 deletions app/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from app.config.database import Base


# TODO: removed as AuthType is now in app.schemas.users
class AuthType(Enum):
LOCAL = "local"
GOOGLE = "google"
Expand Down
89 changes: 89 additions & 0 deletions app/services/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import logging

from authlib.integrations.starlette_client import OAuthError
from fastapi import HTTPException
from fastapi.responses import JSONResponse

from app.config.github import github
from app.models.user import User
from app.schemas.users import AuthType, UserCreate
from app.services.user import create_user
from app.utils.auth import create_access_token, verify_password


def authenticate_user(db, email: str, password: str):
"""Authenticate user with email and password using hashed password."""
user = db.query(User).filter(User.email == email).first()
if not user or not user.password:
return None
if not verify_password(password, user.password):
return None
return user


def login_user(db, email: str, password: str):
"""Login user with email and password. Returns an access token if successful. Raises HTTPException if authentication fails."""
user = authenticate_user(db, email, password)
if not user:
raise HTTPException(status_code=401, detail="Invalid credentials")
access_token = create_access_token({"sub": str(user.id), "email": user.email})
return {"access_token": access_token, "token_type": "bearer"}


def create_github_user(profile, email: str, db):
"""Create a new user from GitHub profile data."""
name = profile.get("name") or profile.get("login")
provider_id = str(profile.get("id"))

user_in = UserCreate(
email=email,
name=name,
is_active=True,
provider_id=provider_id,
auth_type=AuthType.github,
)
user = create_user(db, user_in)
return user


async def login_github_user(request, db):
"""Login user via GitHub OAuth. Returns an access token if successful. Raises HTTPException if authentication fails."""
try:
token = await github.authorize_access_token(request)
resp = await github.get("user", token=token)
profile = resp.json()
email = profile.get("email")

# If email is not public, fetch from emails endpoint
if not email:
emails_resp = await github.get("user/emails", token=token)
emails = emails_resp.json()
email = next(
(
e.get("email")
for e in emails
if e.get("primary") and e.get("verified")
),
None,
)
if not email:
raise HTTPException(
status_code=400, detail="GitHub account has no accessible email."
)

user = db.query(User).filter(User.email == email).first()

# If user does not exist, create a new one
if not user:
user = create_github_user(profile, email, db)

access_token = create_access_token({"sub": str(user.id), "email": user.email})
return JSONResponse({"access_token": access_token, "token_type": "bearer"})
except OAuthError as e:
logging.error("OAuthError occurred during GitHub authentication", exc_info=e)
return JSONResponse(
{
"error": "An error occurred during authentication. Please try again later."
},
status_code=400,
)
Loading