from fastapi import APIRouter, Depends, Request, status from pydantic import BaseModel from sqlalchemy import func, select from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from app.api.errors import http_error 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 http_error(status.HTTP_409_CONFLICT, "username_taken", "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 http_error( status.HTTP_409_CONFLICT, "derivation_index_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 http_error(status.HTTP_401_UNAUTHORIZED, "invalid_credentials", "invalid credentials") return TokenResponse(access_token=create_access_token(user.id), address=user.address)