Initial commit

This commit is contained in:
2026-01-14 10:43:02 +06:00
commit 3c541a2ba5
17 changed files with 526 additions and 0 deletions

6
backend/.env.example Normal file
View 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
View 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
View 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
View 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
View 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
View 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