diff --git a/backend/app/auth.py b/backend/app/auth.py index f7f6041..29eafa4 100644 --- a/backend/app/auth.py +++ b/backend/app/auth.py @@ -719,6 +719,18 @@ def current_user(request: Request, db: Session = Depends(get_db)) -> User: return user +def optional_current_user( + request: Request, db: Session = Depends(get_db) +) -> User | None: + """Like `current_user` but returns None instead of raising 401 when there's no valid session. + Used by the bootstrap /api/me probe so the logged-out landing doesn't log a failed request to + the browser console (a non-2xx fetch is a console error no matter how the app handles it).""" + try: + return current_user(request, db) + except HTTPException: + return None + + def require_human(user: User = Depends(current_user)) -> User: """Reject the shared demo account from actions that need a real Google/YouTube identity or spend the shared quota. Most YouTube paths are already closed to it implicitly (it has diff --git a/backend/app/routes/me.py b/backend/app/routes/me.py index dca46a9..a810f89 100644 --- a/backend/app/routes/me.py +++ b/backend/app/routes/me.py @@ -9,6 +9,7 @@ from app.auth import ( has_read_scope, has_write_scope, is_allowed, + optional_current_user, purge_user, ) from app.db import get_db @@ -67,8 +68,13 @@ def switch_account( @router.get("") def get_me( - user: User = Depends(current_user), db: Session = Depends(get_db) + user: User | None = Depends(optional_current_user), db: Session = Depends(get_db) ) -> dict: + # The app's bootstrap probe: return 200 with `authenticated: False` when logged out (rather + # than 401) so the public landing never logs a failed /api/me to the browser console. Other + # protected endpoints still 401, so mid-session expiry is still caught by the global handler. + if user is None: + return {"authenticated": False} pending_invites = 0 if user.role == "admin": pending_invites = ( @@ -80,6 +86,7 @@ def get_me( or 0 ) return { + "authenticated": True, "id": user.id, "email": user.email, "display_name": user.display_name, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2a86416..c6a473e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,7 +4,6 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { api, getActiveAccount, - HttpError, setActiveAccount, setUnauthorizedHandler, type FeedFilters, @@ -451,8 +450,9 @@ export default function App() { }, [meQuery.data?.id]); // eslint-disable-line react-hooks/exhaustive-deps // Any request that 401s means the session ended server-side. If we were signed in (cached - // `me` exists), reload to the login page at once (option 2 — also covers any click that hits - // the API). Guarded on cached `me` so the public login page's own /api/me 401 can't loop. + // `me` is a user object), reload to the login page at once (also covers any click that hits + // the API). Guarded on a cached user so a stray 401 while logged out (cached `me` is null) + // can't loop the public landing. useEffect(() => { setUnauthorizedHandler(() => { if (queryClient.getQueryData(["me"])) { @@ -646,14 +646,15 @@ export default function App() { return (