Implement Phase 1: User Authentication & Database (Twitch OAuth, Login Page)
This commit is contained in:
79
auth.py
Normal file
79
auth.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# auth.py
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
import httpx
|
||||||
|
from database import get_db, User
|
||||||
|
|
||||||
|
# IMPORTANT: These must be replaced with your actual Twitch application credentials
|
||||||
|
TWITCH_CLIENT_ID = "YOUR_TWITCH_CLIENT_ID"
|
||||||
|
TWITCH_CLIENT_SECRET = "YOUR_TWITCH_CLIENT_SECRET"
|
||||||
|
REDIRECT_URI = "http://localhost:8000/auth/twitch/callback"
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/login/twitch")
|
||||||
|
async def login_with_twitch():
|
||||||
|
# Scopes required to get user's email and channel info
|
||||||
|
scopes = "user:read:email"
|
||||||
|
auth_url = (
|
||||||
|
f"https://id.twitch.tv/oauth2/authorize"
|
||||||
|
f"?client_id={TWITCH_CLIENT_ID}"
|
||||||
|
f"&redirect_uri={REDIRECT_URI}"
|
||||||
|
f"&response_type=code"
|
||||||
|
f"&scope={scopes}"
|
||||||
|
)
|
||||||
|
return RedirectResponse(url=auth_url)
|
||||||
|
|
||||||
|
@router.get("/auth/twitch/callback")
|
||||||
|
async def auth_twitch_callback(code: str, db: Session = Depends(get_db)):
|
||||||
|
# Exchange the authorization code for an access token
|
||||||
|
token_url = "https://id.twitch.tv/oauth2/token"
|
||||||
|
token_data = {
|
||||||
|
"client_id": TWITCH_CLIENT_ID,
|
||||||
|
"client_secret": TWITCH_CLIENT_SECRET,
|
||||||
|
"code": code,
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"redirect_uri": REDIRECT_URI,
|
||||||
|
}
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
token_response = await client.post(token_url, data=token_data)
|
||||||
|
token_json = token_response.json()
|
||||||
|
access_token = token_json.get("access_token")
|
||||||
|
refresh_token = token_json.get("refresh_token")
|
||||||
|
|
||||||
|
if not access_token:
|
||||||
|
return {"error": "Could not fetch access token"}
|
||||||
|
|
||||||
|
# Get user info from Twitch API
|
||||||
|
user_info_url = "https://api.twitch.tv/helix/users"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {access_token}",
|
||||||
|
"Client-Id": TWITCH_CLIENT_ID,
|
||||||
|
}
|
||||||
|
user_response = await client.get(user_info_url, headers=headers)
|
||||||
|
user_data = user_response.json()["data"][0]
|
||||||
|
|
||||||
|
twitch_id = user_data["id"]
|
||||||
|
twitch_username = user_data["login"]
|
||||||
|
|
||||||
|
# Check if user exists in the database, otherwise create them
|
||||||
|
user = db.query(User).filter(User.twitch_id == twitch_id).first()
|
||||||
|
if not user:
|
||||||
|
user = User(
|
||||||
|
twitch_id=twitch_id,
|
||||||
|
twitch_username=twitch_username,
|
||||||
|
twitch_access_token=access_token, # TODO: Encrypt this
|
||||||
|
twitch_refresh_token=refresh_token, # TODO: Encrypt this
|
||||||
|
)
|
||||||
|
db.add(user)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(user)
|
||||||
|
else:
|
||||||
|
# Update tokens for existing user
|
||||||
|
user.twitch_access_token = access_token # TODO: Encrypt this
|
||||||
|
user.twitch_refresh_token = refresh_token # TODO: Encrypt this
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# TODO: Set a session cookie to keep the user logged in
|
||||||
|
return {"message": f"Successfully logged in as {twitch_username}"}
|
||||||
37
database.py
Normal file
37
database.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from sqlalchemy import create_engine, Column, Integer, String
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DATABASE_URL = "sqlite:///./multichat.db"
|
||||||
|
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
Base = declarative_base()
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
twitch_id = Column(String, unique=True, index=True)
|
||||||
|
twitch_username = Column(String)
|
||||||
|
# We will store encrypted tokens
|
||||||
|
twitch_access_token = Column(String)
|
||||||
|
twitch_refresh_token = Column(String)
|
||||||
|
|
||||||
|
youtube_id = Column(String, unique=True, index=True, nullable=True)
|
||||||
|
youtube_username = Column(String, nullable=True)
|
||||||
|
youtube_access_token = Column(String, nullable=True)
|
||||||
|
youtube_refresh_token = Column(String, nullable=True)
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def create_tables():
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
create_tables()
|
||||||
14
login.html
Normal file
14
login.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Login to MultiChat Overlay</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Login to MultiChat Overlay</h1>
|
||||||
|
<p>Connect your streaming accounts to get started.</p>
|
||||||
|
<a href="/auth/login/twitch">
|
||||||
|
<button>Login with Twitch</button>
|
||||||
|
</a>
|
||||||
|
<!-- YouTube login will be added in a later phase -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
57
main.py
Normal file
57
main.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from fastapi import FastAPI, WebSocket
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from starlette.websockets import WebSocketDisconnect
|
||||||
|
|
||||||
|
from chat_listeners import listen_youtube_chat, listen_twitch_chat
|
||||||
|
from auth import router as auth_router # Import the auth router
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
app.include_router(auth_router, prefix="/auth") # Include the auth router
|
||||||
|
|
||||||
|
connected_clients = []
|
||||||
|
|
||||||
|
async def broadcast_message(message: dict):
|
||||||
|
# Convert the message dictionary to a JSON string before sending
|
||||||
|
message_json = json.dumps(message)
|
||||||
|
for client in connected_clients:
|
||||||
|
try:
|
||||||
|
await client.send_text(message_json)
|
||||||
|
except RuntimeError:
|
||||||
|
# Handle cases where client might have disconnected
|
||||||
|
connected_clients.remove(client)
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def startup_event():
|
||||||
|
# Start chat listeners in the background
|
||||||
|
# Replace with actual video ID and Twitch token/channel
|
||||||
|
# For now, using placeholders. These will need to be configured.
|
||||||
|
asyncio.create_task(listen_youtube_chat("YOUR_YOUTUBE_VIDEO_ID", broadcast_message))
|
||||||
|
asyncio.create_task(listen_twitch_chat("YOUR_TWITCH_OAUTH_TOKEN", "YOUR_TWITCH_CHANNEL", broadcast_message))
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def read_root():
|
||||||
|
return {"Hello": "World"}
|
||||||
|
|
||||||
|
@app.get("/login", response_class=HTMLResponse)
|
||||||
|
async def get_login_page():
|
||||||
|
with open("login.html", "r") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
@app.get("/overlay", response_class=HTMLResponse)
|
||||||
|
async def get_overlay():
|
||||||
|
with open("index.html", "r") as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
@app.websocket("/ws")
|
||||||
|
async def websocket_endpoint(websocket: WebSocket):
|
||||||
|
await websocket.accept()
|
||||||
|
connected_clients.append(websocket)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
# Keep the connection alive, or handle incoming messages if needed
|
||||||
|
await websocket.receive_text()
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
connected_clients.remove(websocket)
|
||||||
Reference in New Issue
Block a user