first commit
This commit is contained in:
commit
7a72438fd1
74 changed files with 7548 additions and 0 deletions
130
backend/app/routers/auth.py
Normal file
130
backend/app/routers/auth.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"""
|
||||
Google OAuth 2.0 flow:
|
||||
1. GET /api/auth/google/login → redirect to Google consent screen
|
||||
2. GET /api/auth/google/callback → exchange code for tokens → get user info
|
||||
→ upsert user in MongoDB → issue JWT
|
||||
→ redirect to frontend with token
|
||||
3. GET /api/auth/me → return current user (protected)
|
||||
4. POST /api/auth/logout → client-side only (stateless JWT), just 200
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import RedirectResponse, JSONResponse
|
||||
import httpx
|
||||
from datetime import datetime, timezone
|
||||
from app.config import get_settings
|
||||
from app.database import get_db
|
||||
from app.dependencies import create_access_token, get_current_user
|
||||
from app.models.user import UserOut
|
||||
|
||||
router = APIRouter(prefix="/api/auth", tags=["auth"])
|
||||
settings = get_settings()
|
||||
|
||||
GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
|
||||
GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v3/userinfo"
|
||||
|
||||
SCOPES = "openid email profile"
|
||||
|
||||
|
||||
# ── Step 1: Redirect to Google ─────────────────────────────────
|
||||
@router.get("/google/login")
|
||||
async def google_login():
|
||||
redirect_uri = f"{settings.backend_url}/api/auth/google/callback"
|
||||
params = (
|
||||
f"?client_id={settings.google_client_id}"
|
||||
f"&redirect_uri={redirect_uri}"
|
||||
f"&response_type=code"
|
||||
f"&scope={SCOPES}"
|
||||
f"&access_type=offline"
|
||||
f"&prompt=select_account"
|
||||
)
|
||||
return RedirectResponse(url=GOOGLE_AUTH_URL + params)
|
||||
|
||||
|
||||
# ── Step 2: Callback from Google ───────────────────────────────
|
||||
@router.get("/google/callback")
|
||||
async def google_callback(code: str, db=Depends(get_db)):
|
||||
redirect_uri = f"{settings.backend_url}/api/auth/google/callback"
|
||||
|
||||
# Exchange authorization code for tokens
|
||||
async with httpx.AsyncClient() as client:
|
||||
token_response = await client.post(
|
||||
GOOGLE_TOKEN_URL,
|
||||
data={
|
||||
"code": code,
|
||||
"client_id": settings.google_client_id,
|
||||
"client_secret": settings.google_client_secret,
|
||||
"redirect_uri": redirect_uri,
|
||||
"grant_type": "authorization_code",
|
||||
},
|
||||
)
|
||||
|
||||
if token_response.status_code != 200:
|
||||
error_url = f"{settings.frontend_url}/login?error=google_token_failed"
|
||||
return RedirectResponse(url=error_url)
|
||||
|
||||
tokens = token_response.json()
|
||||
access_token = tokens.get("access_token")
|
||||
|
||||
# Fetch user profile from Google
|
||||
async with httpx.AsyncClient() as client:
|
||||
userinfo_response = await client.get(
|
||||
GOOGLE_USERINFO_URL,
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
|
||||
if userinfo_response.status_code != 200:
|
||||
error_url = f"{settings.frontend_url}/login?error=google_userinfo_failed"
|
||||
return RedirectResponse(url=error_url)
|
||||
|
||||
google_user = userinfo_response.json()
|
||||
google_id = google_user.get("sub")
|
||||
email = google_user.get("email", "")
|
||||
name = google_user.get("name", "")
|
||||
picture = google_user.get("picture", "")
|
||||
|
||||
# Upsert user in MongoDB
|
||||
now = datetime.now(timezone.utc)
|
||||
await db["users"].update_one(
|
||||
{"google_id": google_id},
|
||||
{
|
||||
"$set": {
|
||||
"email": email,
|
||||
"name": name,
|
||||
"picture": picture,
|
||||
"last_login": now,
|
||||
},
|
||||
"$setOnInsert": {
|
||||
"google_id": google_id,
|
||||
"created_at": now,
|
||||
},
|
||||
},
|
||||
upsert=True,
|
||||
)
|
||||
|
||||
# Issue JWT
|
||||
jwt_token = create_access_token(
|
||||
data={"sub": google_id, "email": email, "name": name, "picture": picture}
|
||||
)
|
||||
|
||||
# Redirect to frontend with token
|
||||
frontend_redirect = f"{settings.frontend_url}/auth/callback?token={jwt_token}"
|
||||
return RedirectResponse(url=frontend_redirect)
|
||||
|
||||
|
||||
# ── Get current user (protected) ──────────────────────────────
|
||||
@router.get("/me", response_model=UserOut)
|
||||
async def get_me(current_user: dict = Depends(get_current_user)):
|
||||
return UserOut(
|
||||
google_id=current_user["google_id"],
|
||||
email=current_user["email"],
|
||||
name=current_user["name"],
|
||||
picture=current_user.get("picture"),
|
||||
)
|
||||
|
||||
|
||||
# ── Logout (stateless — client drops the JWT) ─────────────────
|
||||
@router.post("/logout")
|
||||
async def logout():
|
||||
return JSONResponse({"message": "Logged out successfully"})
|
||||
Loading…
Add table
Add a link
Reference in a new issue