110 lines
3.8 KiB
Python
110 lines
3.8 KiB
Python
import httpx
|
|
import secrets
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
|
from fastapi.responses import RedirectResponse
|
|
from sqlalchemy.orm import Session
|
|
|
|
from config import settings
|
|
from database import SessionLocal
|
|
import models
|
|
import security
|
|
|
|
router = APIRouter()
|
|
|
|
# Dependency to get a DB session
|
|
def get_db():
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
|
|
@router.get("/login/twitch")
|
|
async def login_with_twitch(request: Request):
|
|
"""
|
|
Step 1 of OAuth flow: Redirect the user to Twitch's authorization page.
|
|
"""
|
|
# Generate a random state token for CSRF protection
|
|
state = secrets.token_urlsafe(16)
|
|
request.session['oauth_state'] = state
|
|
|
|
# As per RESEARCH_REPORT.md, these are the minimum required scopes
|
|
scopes = "chat:read"
|
|
|
|
# Construct the authorization URL
|
|
auth_url = (
|
|
f"https://id.twitch.tv/oauth2/authorize"
|
|
f"?response_type=code"
|
|
f"&client_id={settings.TWITCH_CLIENT_ID}"
|
|
f"&redirect_uri={settings.APP_BASE_URL}/auth/twitch/callback"
|
|
f"&scope={scopes}"
|
|
f"&state={state}"
|
|
)
|
|
return RedirectResponse(url=auth_url)
|
|
|
|
@router.get("/auth/twitch/callback")
|
|
async def auth_twitch_callback(code: str, state: str, request: Request, db: Session = Depends(get_db)):
|
|
"""
|
|
Step 2 of OAuth flow: Handle the callback from Twitch after user authorization.
|
|
"""
|
|
# CSRF Protection: Validate the state
|
|
if state != request.session.pop('oauth_state', None):
|
|
raise HTTPException(status_code=403, detail="Invalid state parameter. CSRF attack suspected.")
|
|
|
|
# Step 4: Exchange the authorization code for an access token
|
|
token_url = "https://id.twitch.tv/oauth2/token"
|
|
token_data = {
|
|
"client_id": settings.TWITCH_CLIENT_ID,
|
|
"client_secret": settings.TWITCH_CLIENT_SECRET,
|
|
"code": code,
|
|
"grant_type": "authorization_code",
|
|
"redirect_uri": f"{settings.APP_BASE_URL}/auth/twitch/callback",
|
|
}
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
token_response = await client.post(token_url, data=token_data)
|
|
if token_response.status_code != 200:
|
|
raise HTTPException(status_code=400, detail="Failed to retrieve access token from Twitch.")
|
|
|
|
token_json = token_response.json()
|
|
access_token = token_json["access_token"]
|
|
refresh_token = token_json["refresh_token"]
|
|
|
|
# Step 5: Validate the user and get their details from Twitch API
|
|
users_url = "https://api.twitch.tv/helix/users"
|
|
headers = {
|
|
"Client-ID": settings.TWITCH_CLIENT_ID,
|
|
"Authorization": f"Bearer {access_token}",
|
|
}
|
|
user_response = await client.get(users_url, headers=headers)
|
|
user_data = user_response.json()["data"][0]
|
|
|
|
# Encrypt the tokens for storage
|
|
encrypted_tokens = security.encrypt_tokens(access_token, refresh_token)
|
|
|
|
# --- Database Upsert Logic ---
|
|
# Check if the user already exists in our database
|
|
user = db.query(models.User).filter(models.User.platform_user_id == user_data['id']).first()
|
|
|
|
if user:
|
|
# If user exists, update their details
|
|
user.username = user_data['login']
|
|
user.encrypted_tokens = encrypted_tokens
|
|
else:
|
|
# If user does not exist, create a new record
|
|
user = models.User(
|
|
platform_user_id=user_data['id'],
|
|
username=user_data['login'],
|
|
platform="twitch",
|
|
encrypted_tokens=encrypted_tokens
|
|
)
|
|
db.add(user)
|
|
|
|
db.commit()
|
|
|
|
# Create a session for the user by storing their database ID.
|
|
request.session['user_id'] = user.id
|
|
|
|
# Redirect to a future dashboard page for a better user experience
|
|
# This prepares us for Task 1.4 (Session Management) and Task 2.1 (Dashboard UI)
|
|
return RedirectResponse(url="/dashboard") |