Initial commit
This commit is contained in:
6
backend/.env.example
Normal file
6
backend/.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
OIDC_ISSUER=https://accounts.google.com
|
||||
OIDC_CLIENT_ID=your-client-id
|
||||
OIDC_CLIENT_SECRET=your-client-secret
|
||||
OIDC_REDIRECT_URI=http://localhost:3000/callback
|
||||
SECRET_KEY=your-secret-key-change-this
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
80
backend/auth.py
Normal file
80
backend/auth.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from fastapi import HTTPException, Depends, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from jose import jwt, JWTError
|
||||
import httpx
|
||||
from typing import Optional
|
||||
from config import settings
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
class OIDCClient:
|
||||
def __init__(self):
|
||||
self.issuer = settings.oidc_issuer
|
||||
self.client_id = settings.oidc_client_id
|
||||
self.client_secret = settings.oidc_client_secret
|
||||
self.redirect_uri = settings.oidc_redirect_uri
|
||||
self._discovery_cache = None
|
||||
|
||||
async def get_discovery_document(self):
|
||||
if self._discovery_cache:
|
||||
return self._discovery_cache
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{self.issuer}/.well-known/openid-configuration")
|
||||
self._discovery_cache = response.json()
|
||||
return self._discovery_cache
|
||||
|
||||
async def exchange_code(self, code: str):
|
||||
discovery = await self.get_discovery_document()
|
||||
token_endpoint = discovery["token_endpoint"]
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(
|
||||
token_endpoint,
|
||||
data={
|
||||
"grant_type": "authorization_code",
|
||||
"code": code,
|
||||
"redirect_uri": self.redirect_uri,
|
||||
"client_id": self.client_id,
|
||||
"client_secret": self.client_secret,
|
||||
}
|
||||
)
|
||||
return response.json()
|
||||
|
||||
async def get_user_info(self, access_token: str):
|
||||
discovery = await self.get_discovery_document()
|
||||
userinfo_endpoint = discovery["userinfo_endpoint"]
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
userinfo_endpoint,
|
||||
headers={"Authorization": f"Bearer {access_token}"}
|
||||
)
|
||||
return response.json()
|
||||
|
||||
oidc_client = OIDCClient()
|
||||
|
||||
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||||
token = credentials.credentials
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings.secret_key,
|
||||
algorithms=["HS256"]
|
||||
)
|
||||
user_id: str = payload.get("sub")
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials"
|
||||
)
|
||||
return payload
|
||||
except JWTError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication credentials"
|
||||
)
|
||||
|
||||
def create_access_token(data: dict) -> str:
|
||||
return jwt.encode(data, settings.secret_key, algorithm="HS256")
|
||||
14
backend/config.py
Normal file
14
backend/config.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
class Settings(BaseSettings):
|
||||
oidc_issuer: str
|
||||
oidc_client_id: str
|
||||
oidc_client_secret: str
|
||||
oidc_redirect_uri: str
|
||||
secret_key: str
|
||||
frontend_url: str
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
settings = Settings()
|
||||
161
backend/main.py
Normal file
161
backend/main.py
Normal file
@@ -0,0 +1,161 @@
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Depends, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from typing import Dict, List
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from models import User, Stream, StreamCreate, AuthCallback, ChatMessage
|
||||
from auth import oidc_client, get_current_user, create_access_token
|
||||
from config import settings
|
||||
|
||||
app = FastAPI(title="Video Streaming Platform")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=[settings.frontend_url],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# In-memory storage
|
||||
streams: Dict[str, Stream] = {}
|
||||
users: Dict[str, User] = {}
|
||||
active_connections: Dict[str, List[WebSocket]] = {}
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Video Streaming API"}
|
||||
|
||||
@app.get("/auth/login")
|
||||
async def login():
|
||||
discovery = await oidc_client.get_discovery_document()
|
||||
auth_endpoint = discovery["authorization_endpoint"]
|
||||
|
||||
auth_url = (
|
||||
f"{auth_endpoint}?"
|
||||
f"client_id={settings.oidc_client_id}&"
|
||||
f"redirect_uri={settings.oidc_redirect_uri}&"
|
||||
f"response_type=code&"
|
||||
f"scope=openid email profile"
|
||||
)
|
||||
|
||||
return {"auth_url": auth_url}
|
||||
|
||||
@app.post("/auth/callback")
|
||||
async def auth_callback(callback: AuthCallback):
|
||||
try:
|
||||
token_response = await oidc_client.exchange_code(callback.code)
|
||||
access_token = token_response.get("access_token")
|
||||
|
||||
if not access_token:
|
||||
raise HTTPException(status_code=400, detail="Failed to get access token")
|
||||
|
||||
user_info = await oidc_client.get_user_info(access_token)
|
||||
|
||||
user = User(
|
||||
id=user_info.get("sub"),
|
||||
email=user_info.get("email"),
|
||||
name=user_info.get("name", user_info.get("email")),
|
||||
picture=user_info.get("picture")
|
||||
)
|
||||
|
||||
users[user.id] = user
|
||||
|
||||
jwt_token = create_access_token({
|
||||
"sub": user.id,
|
||||
"email": user.email,
|
||||
"name": user.name
|
||||
})
|
||||
|
||||
return {"access_token": jwt_token, "user": user}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
@app.get("/streams")
|
||||
async def get_streams():
|
||||
return list(streams.values())
|
||||
|
||||
@app.get("/streams/{stream_id}")
|
||||
async def get_stream(stream_id: str):
|
||||
if stream_id not in streams:
|
||||
raise HTTPException(status_code=404, detail="Stream not found")
|
||||
return streams[stream_id]
|
||||
|
||||
@app.post("/streams")
|
||||
async def create_stream(
|
||||
stream_data: StreamCreate,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
stream_id = str(uuid.uuid4())
|
||||
stream = Stream(
|
||||
id=stream_id,
|
||||
user_id=current_user["sub"],
|
||||
title=stream_data.title,
|
||||
description=stream_data.description,
|
||||
is_live=True,
|
||||
created_at=datetime.now()
|
||||
)
|
||||
|
||||
streams[stream_id] = stream
|
||||
active_connections[stream_id] = []
|
||||
|
||||
return stream
|
||||
|
||||
@app.delete("/streams/{stream_id}")
|
||||
async def end_stream(
|
||||
stream_id: str,
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
if stream_id not in streams:
|
||||
raise HTTPException(status_code=404, detail="Stream not found")
|
||||
|
||||
stream = streams[stream_id]
|
||||
if stream.user_id != current_user["sub"]:
|
||||
raise HTTPException(status_code=403, detail="Not authorized")
|
||||
|
||||
stream.is_live = False
|
||||
|
||||
for connection in active_connections.get(stream_id, []):
|
||||
await connection.close()
|
||||
|
||||
active_connections[stream_id] = []
|
||||
|
||||
return {"message": "Stream ended"}
|
||||
|
||||
@app.websocket("/ws/stream/{stream_id}")
|
||||
async def websocket_stream(websocket: WebSocket, stream_id: str):
|
||||
await websocket.accept()
|
||||
|
||||
if stream_id not in streams:
|
||||
await websocket.close(code=1008)
|
||||
return
|
||||
|
||||
if stream_id not in active_connections:
|
||||
active_connections[stream_id] = []
|
||||
|
||||
active_connections[stream_id].append(websocket)
|
||||
streams[stream_id].viewer_count = len(active_connections[stream_id])
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_json()
|
||||
|
||||
for connection in active_connections[stream_id]:
|
||||
if connection != websocket:
|
||||
await connection.send_json(data)
|
||||
|
||||
except WebSocketDisconnect:
|
||||
active_connections[stream_id].remove(websocket)
|
||||
streams[stream_id].viewer_count = len(active_connections[stream_id])
|
||||
|
||||
@app.get("/me")
|
||||
async def get_me(current_user: dict = Depends(get_current_user)):
|
||||
user_id = current_user["sub"]
|
||||
if user_id in users:
|
||||
return users[user_id]
|
||||
return current_user
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
32
backend/models.py
Normal file
32
backend/models.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
class User(BaseModel):
|
||||
id: str
|
||||
email: str
|
||||
name: str
|
||||
picture: Optional[str] = None
|
||||
|
||||
class Stream(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
is_live: bool
|
||||
viewer_count: int = 0
|
||||
created_at: datetime
|
||||
thumbnail: Optional[str] = None
|
||||
|
||||
class StreamCreate(BaseModel):
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
|
||||
class AuthCallback(BaseModel):
|
||||
code: str
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
user_id: str
|
||||
username: str
|
||||
message: str
|
||||
timestamp: datetime
|
||||
10
backend/requirements.txt
Normal file
10
backend/requirements.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
fastapi==0.109.0
|
||||
uvicorn[standard]==0.27.0
|
||||
python-jose[cryptography]==3.3.0
|
||||
python-multipart==0.0.6
|
||||
pydantic==2.5.3
|
||||
pydantic-settings==2.1.0
|
||||
httpx==0.26.0
|
||||
python-dotenv==1.0.0
|
||||
websockets==12.0
|
||||
aiofiles==23.2.1
|
||||
Reference in New Issue
Block a user