feat(auth): add DB-backed login, token management, and idempotent auth migration
This commit is contained in:
+113
-33
@@ -1,8 +1,10 @@
|
||||
"""Portfolio API routes."""
|
||||
|
||||
from fastapi import APIRouter, Depends, Response, status
|
||||
from fastapi import APIRouter, Depends, Request, Response, status
|
||||
|
||||
from app.core.auth import require_admin, require_read, require_trade_write
|
||||
from app.core.auth import Permission, authorize_request
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.repositories.factory import get_portfolio_repository_from_settings
|
||||
from app.schemas.portfolio import (
|
||||
BuyRequest,
|
||||
CreatePortfolioRequest,
|
||||
@@ -14,101 +16,179 @@ from app.schemas.portfolio import (
|
||||
TransferRequest,
|
||||
UpdatePortfolioRequest,
|
||||
)
|
||||
from app.services.portfolio_service import PortfolioService, get_portfolio_service
|
||||
from app.services.portfolio_service import PortfolioService
|
||||
|
||||
router = APIRouter(prefix="/portfolios", tags=["portfolios"])
|
||||
|
||||
|
||||
@router.get("", response_model=list[PortfolioListItem], dependencies=[Depends(require_read)])
|
||||
def _get_service(settings: Settings) -> tuple[PortfolioService, object]:
|
||||
repository = get_portfolio_repository_from_settings(settings)
|
||||
return PortfolioService(repository), repository
|
||||
|
||||
|
||||
def _close_repository(repository: object) -> None:
|
||||
if callable(getattr(type(repository), "close", None)):
|
||||
repository.close()
|
||||
|
||||
|
||||
@router.get("", response_model=list[PortfolioListItem])
|
||||
def list_portfolios(
|
||||
request: Request,
|
||||
broker: str | None = None,
|
||||
status_filter: str | None = None,
|
||||
service: PortfolioService = Depends(get_portfolio_service),
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> list[PortfolioListItem]:
|
||||
return service.list_portfolios(broker=broker, status=status_filter)
|
||||
authorize_request(Permission.READ, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
return service.list_portfolios(broker=broker, status=status_filter)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
|
||||
|
||||
@router.get("/{portfolio_id}", response_model=PortfolioDetailResponse, dependencies=[Depends(require_read)])
|
||||
@router.get("/{portfolio_id}", response_model=PortfolioDetailResponse)
|
||||
def get_portfolio(
|
||||
portfolio_id: str,
|
||||
request: Request,
|
||||
*,
|
||||
recent_trades: int = 5,
|
||||
service: PortfolioService = Depends(get_portfolio_service),
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
return service.get_portfolio_status(portfolio_id=portfolio_id, recent_trades=recent_trades)
|
||||
authorize_request(Permission.READ, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
return service.get_portfolio_status(portfolio_id=portfolio_id, recent_trades=recent_trades)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=PortfolioDetailResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(require_trade_write)],
|
||||
)
|
||||
def create_portfolio(
|
||||
payload: CreatePortfolioRequest,
|
||||
service: PortfolioService = Depends(get_portfolio_service),
|
||||
request: Request,
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
return service.create_portfolio(payload)
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
return service.create_portfolio(payload)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
|
||||
|
||||
@router.patch("/{portfolio_id}", response_model=PortfolioDetailResponse, dependencies=[Depends(require_trade_write)])
|
||||
@router.patch("/{portfolio_id}", response_model=PortfolioDetailResponse)
|
||||
def update_portfolio(
|
||||
portfolio_id: str,
|
||||
payload: UpdatePortfolioRequest,
|
||||
service: PortfolioService = Depends(get_portfolio_service),
|
||||
request: Request,
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
return service.update_portfolio(portfolio_id, payload)
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
return service.update_portfolio(portfolio_id, payload)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
|
||||
|
||||
@router.post("/{portfolio_id}/buy", response_model=PortfolioDetailResponse, dependencies=[Depends(require_trade_write)])
|
||||
@router.post("/{portfolio_id}/buy", response_model=PortfolioDetailResponse)
|
||||
def buy_portfolio_asset(
|
||||
portfolio_id: str,
|
||||
payload: BuyRequest,
|
||||
service: PortfolioService = Depends(get_portfolio_service),
|
||||
request: Request,
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
return service.buy(portfolio_id, payload)
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
return service.buy(portfolio_id, payload)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
|
||||
|
||||
@router.post("/{portfolio_id}/sell", response_model=PortfolioDetailResponse, dependencies=[Depends(require_trade_write)])
|
||||
@router.post("/{portfolio_id}/sell", response_model=PortfolioDetailResponse)
|
||||
def sell_portfolio_asset(
|
||||
portfolio_id: str,
|
||||
payload: SellRequest,
|
||||
service: PortfolioService = Depends(get_portfolio_service),
|
||||
request: Request,
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
return service.sell(portfolio_id, payload)
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
return service.sell(portfolio_id, payload)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
|
||||
|
||||
@router.post("/{portfolio_id}/invest", response_model=PortfolioDetailResponse, dependencies=[Depends(require_trade_write)])
|
||||
@router.post("/{portfolio_id}/invest", response_model=PortfolioDetailResponse)
|
||||
def invest_buffer(
|
||||
portfolio_id: str,
|
||||
payload: InvestRequest,
|
||||
service: PortfolioService = Depends(get_portfolio_service),
|
||||
request: Request,
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
return service.invest(portfolio_id, payload)
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
return service.invest(portfolio_id, payload)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
|
||||
|
||||
@router.post("/{portfolio_id}/transfer", response_model=PortfolioDetailResponse, dependencies=[Depends(require_trade_write)])
|
||||
@router.post("/{portfolio_id}/transfer", response_model=PortfolioDetailResponse)
|
||||
def transfer_buffer(
|
||||
portfolio_id: str,
|
||||
payload: TransferRequest,
|
||||
service: PortfolioService = Depends(get_portfolio_service),
|
||||
request: Request,
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
return service.transfer(portfolio_id, payload)
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
return service.transfer(portfolio_id, payload)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
|
||||
|
||||
@router.post("/{portfolio_id}/reset", response_model=PortfolioDetailResponse, dependencies=[Depends(require_trade_write)])
|
||||
@router.post("/{portfolio_id}/reset", response_model=PortfolioDetailResponse)
|
||||
def reset_portfolio(
|
||||
portfolio_id: str,
|
||||
payload: ResetPortfolioRequest,
|
||||
service: PortfolioService = Depends(get_portfolio_service),
|
||||
request: Request,
|
||||
*,
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> PortfolioDetailResponse:
|
||||
return service.reset(portfolio_id, payload)
|
||||
authorize_request(Permission.TRADE_WRITE, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
return service.reset(portfolio_id, payload)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
|
||||
|
||||
@router.delete("/{portfolio_id}", status_code=status.HTTP_204_NO_CONTENT, dependencies=[Depends(require_admin)])
|
||||
@router.delete("/{portfolio_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_portfolio(
|
||||
portfolio_id: str,
|
||||
request: Request,
|
||||
*,
|
||||
keep_ledger: bool = False,
|
||||
service: PortfolioService = Depends(get_portfolio_service),
|
||||
settings: Settings = Depends(get_settings),
|
||||
) -> Response:
|
||||
service.delete(portfolio_id, keep_ledger=keep_ledger)
|
||||
authorize_request(Permission.ADMIN, request, settings)
|
||||
service, repository = _get_service(settings)
|
||||
try:
|
||||
service.delete(portfolio_id, keep_ledger=keep_ledger)
|
||||
finally:
|
||||
_close_repository(repository)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
Reference in New Issue
Block a user