|
23 | 23 |
|
24 | 24 | import bcrypt |
25 | 25 | import jwt |
26 | | -from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request |
| 26 | +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Request, Response |
27 | 27 | from pydantic import BaseModel, Field |
28 | 28 |
|
29 | 29 | from . import aigrp |
@@ -405,32 +405,44 @@ async def require_admin( |
405 | 405 |
|
406 | 406 |
|
407 | 407 | @router.post("/login") |
408 | | -async def login(request: LoginRequest, store: SqliteStore = Depends(get_store)) -> LoginResponse: |
409 | | - """Authenticate a user and return a JWT token. |
| 408 | +async def login( |
| 409 | + request: LoginRequest, |
| 410 | + response: Response, |
| 411 | + store: SqliteStore = Depends(get_store), |
| 412 | +) -> LoginResponse: |
| 413 | + """Authenticate a user, set the cq_session cookie, and return a JWT. |
| 414 | +
|
| 415 | + Post-FO-1d the React admin app reads its session from the |
| 416 | + HttpOnly cookie rather than localStorage (#199). The JWT in the |
| 417 | + response body is kept for non-browser callers (curl scripts, the |
| 418 | + 8l-cli) that send ``Authorization: Bearer`` instead. |
410 | 419 |
|
411 | 420 | Args: |
412 | 421 | request: Login credentials. |
| 422 | + response: FastAPI response — used to set the session cookie. |
413 | 423 | store: The store dependency. |
414 | 424 |
|
415 | 425 | Returns: |
416 | | - A LoginResponse with a signed JWT and the username. |
| 426 | + A LoginResponse with a signed JWT + username. Browsers don't |
| 427 | + need to read the JWT; the cookie carries the session. |
417 | 428 |
|
418 | 429 | Raises: |
419 | 430 | HTTPException: With status 401 if credentials are invalid. |
420 | 431 | """ |
| 432 | + from .web_session import mint_session_cookie |
| 433 | + |
421 | 434 | user = await store.get_user(request.username) |
422 | 435 | if user is None or not verify_password(request.password, user["password_hash"]): |
423 | 436 | raise HTTPException(status_code=401, detail="Invalid username or password") |
424 | 437 | # H-1: refuse session minting when the user's persona is soft-disabled. |
425 | 438 | assignment = await store.get_persona_assignment(request.username) |
426 | 439 | if assignment is not None and assignment.get("disabled_at") is not None: |
427 | 440 | raise HTTPException(status_code=403, detail="user is disabled") |
428 | | - token = create_token( |
429 | | - request.username, |
430 | | - secret=_get_jwt_secret(), |
431 | | - aud=SESSION_AUDIENCE, |
432 | | - ) |
433 | | - return LoginResponse(token=token, username=request.username) |
| 441 | + # Set the HttpOnly + SameSite=Lax cq_session cookie. Without this |
| 442 | + # the post-#199 React app has no way to authenticate subsequent |
| 443 | + # requests — localStorage token storage was removed by FO-1d. |
| 444 | + session_token = mint_session_cookie(response, username=request.username) |
| 445 | + return LoginResponse(token=session_token, username=request.username) |
434 | 446 |
|
435 | 447 |
|
436 | 448 | @dataclass |
|
0 commit comments