Merge watch profiles into user accounts — one profile per login, no picker

- Removed the "Who's Watching" sub-profile picker entirely: ProfileProvider now
  auto-resolves the single profile tied to the account immediately after login
- Backend: removed POST/DELETE /profiles (no more creating/deleting extra
  profiles — lifecycle is entirely driven by user create/delete, already wired
  in the admin Users CRUD); kept GET (internal) and PATCH (edit your own
  profile's avatar/kids-mode/rating cap)
- Deleted ProfileSelect page and the now-unused ProtectedRoute component
- Navbar: removed the "switch profile" action, account avatar is now just a
  static display
- api.js: dropped the now-dead X-Profile-Id header logic (backend already
  falls back to the account's single profile when the header is absent)
This commit is contained in:
Myron Blair
2026-07-26 19:40:12 -05:00
parent 5b191da471
commit f7626ced2e
8 changed files with 22 additions and 262 deletions
+5 -25
View File
@@ -17,7 +17,7 @@ from motor.motor_asyncio import AsyncIOMotorClient
from models import (
UserCreate, UserLogin, UserPublic, TokenResponse, PasswordChange,
AdminUserCreate, AdminUserUpdate,
Profile, ProfileCreate, ProfileUpdate, RATING_ORDER,
Profile, ProfileUpdate, RATING_ORDER,
Movie, MovieCreate, MovieUpdate,
Subtitle,
WatchlistItem, ProgressUpsert,
@@ -308,25 +308,17 @@ async def admin_delete_user(user_id: str, user: dict = Depends(require_admin)):
return {"ok": True}
# ============ PROFILES ============
# ============ PROFILE (1:1 with account — no separate sub-profile management) ============
@api.get("/profiles", response_model=List[Profile])
async def list_profiles(user: dict = Depends(get_current_user)):
docs = await db.profiles.find({"user_id": user["id"]}, {"_id": 0}).sort("created_at", 1).to_list(20)
"""Always exactly one profile per account; get_active_profile auto-creates it if missing."""
docs = await db.profiles.find({"user_id": user["id"]}, {"_id": 0}).sort("created_at", 1).to_list(1)
return docs
@api.post("/profiles", response_model=Profile)
async def create_profile(payload: ProfileCreate, user: dict = Depends(get_current_user)):
count = await db.profiles.count_documents({"user_id": user["id"]})
if count >= 5:
raise HTTPException(status_code=400, detail="Max 5 profiles per account")
prof = Profile(user_id=user["id"], **payload.model_dump()).model_dump()
await db.profiles.insert_one(prof)
return _strip(prof)
@api.patch("/profiles/{pid}", response_model=Profile)
async def update_profile(pid: str, payload: ProfileUpdate, user: dict = Depends(get_current_user)):
"""Edit your own single profile (avatar color, kids mode, rating cap) — not a multi-profile switcher."""
updates = {k: v for k, v in payload.model_dump().items() if v is not None}
if not updates:
raise HTTPException(status_code=400, detail="No fields to update")
@@ -340,18 +332,6 @@ async def update_profile(pid: str, payload: ProfileUpdate, user: dict = Depends(
return res
@api.delete("/profiles/{pid}")
async def delete_profile(pid: str, user: dict = Depends(get_current_user)):
if await db.profiles.count_documents({"user_id": user["id"]}) <= 1:
raise HTTPException(status_code=400, detail="Cannot delete the only profile")
res = await db.profiles.delete_one({"id": pid, "user_id": user["id"]})
if res.deleted_count == 0:
raise HTTPException(status_code=404, detail="Profile not found")
await db.watchlist.delete_many({"profile_id": pid})
await db.progress.delete_many({"profile_id": pid})
return {"ok": True}
# ============ MOVIES ============
def _movie_filter_for_profile(profile: dict) -> dict:
cap = profile.get("max_rating", "NR")