Balance display: place_bet/request_withdrawal spend whole UTXOs and mark them spent at broadcast time, well before confirmation, so the confirmed-only balance could drop by far more than the amount actually moving. Add compute_pending_balance() (app/wallet/balance.py) to fold the unconfirmed change from in-flight bet/withdrawal PendingTransactions back in; GET /users/me now returns pending_balance_sats + has_pending, and the frontend shows it colored green (settled) or amber (still pending) instead of the confirmed-only figure. Round outcome display: the win/lose reveal and the "pagamento al vincitore in corso" status were fighting over the same UI slot, and the reveal broke across a page refresh. Now: - The round-status box (generic phase progress) and the personal win/lose box are independent and can both be visible at once. - The win/lose box only renders for users who actually played in that round (new user_played field on GET /rounds/current, via a new optional-auth dependency so the endpoint stays usable logged-out). - The reveal delay is anchored to the round's server-provided closes_at instead of a client-side "first seen" timestamp, so repeated reloads can't reset it, and the revealed result is persisted in localStorage so it survives a refresh even after the round has fully closed. - GET /users/me/last-round-result is a durable DB-backed backstop for players who miss the live window entirely (backgrounded tab, offline). Also hardens the frontend polling loop: call() now times out instead of hanging forever, and a session-epoch counter stops an in-flight request from a previous login from resurrecting a duplicate poll loop after logout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
43 lines
1.5 KiB
Python
43 lines
1.5 KiB
Python
from fastapi import Depends, HTTPException, Request, status
|
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.auth.security import decode_access_token
|
|
from app.db.models import User
|
|
from app.db.session import get_session
|
|
|
|
_bearer = HTTPBearer()
|
|
|
|
|
|
async def get_current_user(
|
|
credentials: HTTPAuthorizationCredentials = Depends(_bearer),
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> User:
|
|
try:
|
|
user_id = decode_access_token(credentials.credentials)
|
|
except Exception as exc:
|
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "invalid token") from exc
|
|
|
|
user = await session.scalar(select(User).where(User.id == user_id))
|
|
if user is None:
|
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "user not found")
|
|
return user
|
|
|
|
|
|
async def get_optional_user(
|
|
request: Request,
|
|
session: AsyncSession = Depends(get_session),
|
|
) -> User | None:
|
|
"""Like get_current_user, but for endpoints reachable both logged-out and
|
|
logged-in (e.g. /rounds/current) that need to personalize their response
|
|
*if* the caller happens to be authenticated, without requiring it."""
|
|
auth_header = request.headers.get("Authorization", "")
|
|
if not auth_header.startswith("Bearer "):
|
|
return None
|
|
try:
|
|
user_id = decode_access_token(auth_header.removeprefix("Bearer "))
|
|
except Exception:
|
|
return None
|
|
return await session.scalar(select(User).where(User.id == user_id))
|