Recover from a dead session, and report a node problem once
An expired or orphaned session left the app on a half-rendered page instead of the login screen: a properly signed token naming a user who no longer exists answered 404, which the client does not treat as an authentication failure. All failures in get_current_user are 401 now, and the client stops retrying them, so a dead session goes straight back to the login screen. - Clicking an edge names the two nodes it runs between, not just the message. - A node with a problem shows one badge carrying the whole explanation, rather than a corner badge and a status dot saying the same thing twice. The dot is back to what it is good at: whether the node ran. - The sidebar's collapse control sits in the sidebar, where a phone still finds one in the chrome because there is no sidebar on screen to hold it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WzrvW7rjQbynnhF6pxh6i
This commit is contained in:
co-authored by
Claude Fable 5
parent
6fb42bb3ef
commit
c254d487ba
+12
-5
@@ -47,6 +47,12 @@ def user_from_token(session: Session, token: str) -> User | None:
|
||||
|
||||
|
||||
def get_current_user(session: SessionDep, token: TokenDep) -> User:
|
||||
"""Resolve the bearer token to its user.
|
||||
|
||||
Every failure here is an authentication failure, so all of them answer 401:
|
||||
a token naming a user who no longer exists is a session to log in again,
|
||||
not a missing resource to report.
|
||||
"""
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
|
||||
@@ -54,14 +60,15 @@ def get_current_user(session: SessionDep, token: TokenDep) -> User:
|
||||
token_data = TokenPayload(**payload)
|
||||
except (InvalidTokenError, ValidationError):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
)
|
||||
user = session.get(User, token_data.sub)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
if user is None or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="This session is no longer valid — please log in again",
|
||||
)
|
||||
return user
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""A session the server will not accept must say so as an auth failure.
|
||||
|
||||
The client clears the stored token and returns to the login screen on 401. Any
|
||||
other status leaves the browser holding a token that can never work, stuck on a
|
||||
page that will not load.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.core import security
|
||||
from app.core.config import settings
|
||||
|
||||
PROTECTED = [
|
||||
f"{settings.API_V1_STR}/users/me",
|
||||
f"{settings.API_V1_STR}/flows/",
|
||||
]
|
||||
|
||||
|
||||
def token_for(subject: str) -> str:
|
||||
return security.create_access_token(subject, timedelta(days=1))
|
||||
|
||||
|
||||
def test_no_token_is_unauthorised(client: TestClient) -> None:
|
||||
for path in PROTECTED:
|
||||
assert client.get(path).status_code == 401
|
||||
|
||||
|
||||
def test_a_token_we_did_not_sign_is_unauthorised(client: TestClient) -> None:
|
||||
headers = {"Authorization": "Bearer not.a.real.token"}
|
||||
for path in PROTECTED:
|
||||
assert client.get(path, headers=headers).status_code == 401
|
||||
|
||||
|
||||
def test_a_token_for_a_user_who_is_gone_is_unauthorised(
|
||||
client: TestClient,
|
||||
) -> None:
|
||||
# Properly signed, but the user it names no longer exists — which is what a
|
||||
# browser holds after the database is reset.
|
||||
headers = {"Authorization": f"Bearer {token_for(str(uuid.uuid4()))}"}
|
||||
for path in PROTECTED:
|
||||
response = client.get(path, headers=headers)
|
||||
assert response.status_code == 401, path
|
||||
Reference in New Issue
Block a user