Files
plm-lottery/app/auth/routes.py
T
davideandClaude Sonnet 5 df72367f02 Implement full MVP: auth, HD wallet, Electrum client, deposits, bets, round/draw engine, payout, withdrawals, RBF, admin+audit
All 10 build-order stages complete and unit-tested (49 tests). Verified live on
mainnet: registration/address derivation, deposit crediting, a real 10 PLM bet
(broadcast + confirmed + change credited). A full round close->draw->payout
cycle was triggered live and was in progress at commit time. Withdrawal and RBF
bump are unit-tested but not yet exercised against a live broadcast. Known gaps
(scheduler doesn't resume mid-flight rounds after restart, payout has no retry,
no deployment setup, etc.) are documented in CLAUDE.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 23:52:20 +02:00

71 lines
2.5 KiB
Python

from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth.security import create_access_token, hash_password, verify_password
from app.db.models import User
from app.db.session import get_session
from app.wallet.hd import derive_user_address
router = APIRouter(prefix="/auth", tags=["auth"])
_MAX_REGISTER_RETRIES = 5
class RegisterRequest(BaseModel):
username: str
password: str
class TokenResponse(BaseModel):
access_token: str
address: str
@router.post("/register", response_model=TokenResponse, status_code=status.HTTP_201_CREATED)
async def register(
body: RegisterRequest, request: Request, session: AsyncSession = Depends(get_session)
) -> TokenResponse:
existing = await session.scalar(select(User).where(User.username == body.username))
if existing is not None:
raise HTTPException(status.HTTP_409_CONFLICT, "username already taken")
password_hash = hash_password(body.password)
for _ in range(_MAX_REGISTER_RETRIES):
max_index = await session.scalar(select(func.max(User.derivation_index)))
next_index = 0 if max_index is None else max_index + 1
address = derive_user_address(next_index)
user = User(
username=body.username,
password_hash=password_hash,
derivation_index=next_index,
address=address,
)
session.add(user)
try:
await session.commit()
except IntegrityError:
await session.rollback()
continue
await session.refresh(user)
request.app.state.electrum_listener.address_for_new_user(user.id, user.address)
return TokenResponse(access_token=create_access_token(user.id), address=user.address)
raise HTTPException(status.HTTP_409_CONFLICT, "could not allocate a derivation index, retry")
class LoginRequest(BaseModel):
username: str
password: str
@router.post("/login", response_model=TokenResponse)
async def login(body: LoginRequest, session: AsyncSession = Depends(get_session)) -> TokenResponse:
user = await session.scalar(select(User).where(User.username == body.username))
if user is None or not verify_password(body.password, user.password_hash):
raise HTTPException(status.HTTP_401_UNAUTHORIZED, "invalid credentials")
return TokenResponse(access_token=create_access_token(user.id), address=user.address)