Add random 4-digit TV token per child for obfuscated TV URLs
Each child is assigned a unique permanent tv_token on creation. The TV dashboard URL (/tv/:tvToken) and WebSocket (/ws/:tvToken) now use this token instead of the internal DB ID. Existing children are backfilled on startup. README updated to reflect the change. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import random
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
@@ -9,6 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.config import get_settings
|
||||
from app.database import engine
|
||||
from app.models import Base
|
||||
from app.models.child import Child
|
||||
from app.models.subject import Subject
|
||||
from app.models.user import User
|
||||
from app.routers import auth, users, children, subjects, schedules, sessions, logs, dashboard
|
||||
@@ -41,9 +43,27 @@ async def lifespan(app: FastAPI):
|
||||
await _add_column_if_missing(conn, "subjects", "is_system", "TINYINT(1) NOT NULL DEFAULT 0")
|
||||
await _add_column_if_missing(conn, "users", "last_active_at", "DATETIME NULL")
|
||||
await _add_column_if_missing(conn, "children", "strikes_last_reset", "DATE NULL")
|
||||
await _add_column_if_missing(conn, "children", "tv_token", "INT NULL")
|
||||
|
||||
# Backfill tv_token for existing children that don't have one
|
||||
from app.database import AsyncSessionLocal
|
||||
async with AsyncSessionLocal() as db:
|
||||
result = await db.execute(select(Child).where(Child.tv_token == None)) # noqa: E711
|
||||
children_without_token = result.scalars().all()
|
||||
used_tokens = set()
|
||||
for child in children_without_token:
|
||||
while True:
|
||||
token = random.randint(1000, 9999)
|
||||
if token not in used_tokens:
|
||||
existing = await db.execute(select(Child).where(Child.tv_token == token))
|
||||
if not existing.scalar_one_or_none():
|
||||
break
|
||||
child.tv_token = token
|
||||
used_tokens.add(token)
|
||||
if children_without_token:
|
||||
await db.commit()
|
||||
|
||||
# Seed Meeting system subject for any existing users who don't have one
|
||||
from app.database import AsyncSessionLocal
|
||||
async with AsyncSessionLocal() as db:
|
||||
users_result = await db.execute(select(User).where(User.is_active == True))
|
||||
all_users = users_result.scalars().all()
|
||||
@@ -100,12 +120,19 @@ async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@app.websocket("/ws/{child_id}")
|
||||
async def websocket_endpoint(websocket: WebSocket, child_id: int):
|
||||
@app.websocket("/ws/{tv_token}")
|
||||
async def websocket_endpoint(websocket: WebSocket, tv_token: int):
|
||||
from app.database import AsyncSessionLocal
|
||||
async with AsyncSessionLocal() as db:
|
||||
result = await db.execute(select(Child).where(Child.tv_token == tv_token))
|
||||
child = result.scalar_one_or_none()
|
||||
if not child:
|
||||
await websocket.close(code=4004)
|
||||
return
|
||||
child_id = child.id
|
||||
await manager.connect(websocket, child_id)
|
||||
try:
|
||||
while True:
|
||||
# Keep connection alive; TV clients are receive-only
|
||||
await websocket.receive_text()
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket, child_id)
|
||||
|
||||
Reference in New Issue
Block a user