Security hardening: go-live review fixes

- TV tokens upgraded from 4 to 6 digits; Regen Token button in Admin
- Nginx rate limiting on TV dashboard and WebSocket endpoints
- Login lockout after 5 failed attempts (15 min); clears on admin password reset
- HSTS header added; CSP unsafe-inline removed from script-src; CORS restricted to explicit methods/headers
- Dependency CVE fixes: PyJWT 2.12.0, aiomysql 0.3.0, cryptography 46.0.5, python-multipart 0.0.22
- datetime.utcnow() replaced with datetime.now(timezone.utc) throughout
- SQL identifier whitelist for startup migration queries
- README updated: security notes section, lockout docs, token regen, NPM proxy guidance

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-22 00:00:14 -07:00
parent be86cae7fa
commit 3022bc328b
11 changed files with 228 additions and 30 deletions

View File

@@ -1,7 +1,10 @@
import logging
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, delete
logger = logging.getLogger("homeschool.admin")
from app.auth.jwt import create_admin_token, create_access_token, hash_password
from app.config import get_settings
from app.dependencies import get_db, get_admin_user
@@ -16,6 +19,7 @@ async def admin_login(body: dict):
username = body.get("username", "")
password = body.get("password", "")
if username != settings.admin_username or password != settings.admin_password:
logger.warning("Failed super-admin login attempt for username=%s", username)
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid admin credentials")
token = create_admin_token({"sub": "admin"})
return {"access_token": token, "token_type": "bearer"}
@@ -87,6 +91,8 @@ async def reset_user_password(
if not user:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
user.hashed_password = hash_password(new_password)
user.failed_login_attempts = 0
user.locked_until = None
await db.commit()
return {"ok": True}