feat(v2): client CRUD + balance summary endpoints (P3a)
Adds 6 operator-scoped LP management endpoints:
POST /api/v1/dca/clients — register LP at a machine
GET /api/v1/dca/clients — operator's LPs (all)
GET /api/v1/dca/clients?machine_id=X — scoped to one machine
GET /api/v1/dca/clients/{id} — single LP
PUT /api/v1/dca/clients/{id} — update mode/autoforward/etc
DELETE /api/v1/dca/clients/{id} — delete
GET /api/v1/dca/clients/{id}/balance — fiat balance summary
Ownership transitively checked via the LP's machine — operators can
only see/modify LPs at machines they own. New _machine_owned_by and
_client_owned_by helpers consolidate the 404-not-403 ownership pattern.
Refs: aiolabs/satmachineadmin#9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
56be3e5c52
commit
7226b8289d
1 changed files with 112 additions and 0 deletions
112
views_api.py
112
views_api.py
|
|
@ -12,8 +12,14 @@ from lnbits.core.models import User
|
|||
from lnbits.decorators import check_super_user, check_user_exists
|
||||
|
||||
from .crud import (
|
||||
create_dca_client,
|
||||
create_machine,
|
||||
delete_dca_client,
|
||||
delete_machine,
|
||||
get_client_balance_summary,
|
||||
get_dca_client,
|
||||
get_dca_clients_for_machine,
|
||||
get_dca_clients_for_operator,
|
||||
get_machine,
|
||||
get_machines_for_operator,
|
||||
get_payments_for_operator,
|
||||
|
|
@ -21,15 +27,20 @@ from .crud import (
|
|||
get_settlements_for_machine,
|
||||
get_settlements_for_operator,
|
||||
get_super_config,
|
||||
update_dca_client,
|
||||
update_machine,
|
||||
update_super_config,
|
||||
)
|
||||
from .models import (
|
||||
ClientBalanceSummary,
|
||||
CreateDcaClientData,
|
||||
CreateMachineData,
|
||||
DcaClient,
|
||||
DcaPayment,
|
||||
DcaSettlement,
|
||||
Machine,
|
||||
SuperConfig,
|
||||
UpdateDcaClientData,
|
||||
UpdateMachineData,
|
||||
UpdateSuperConfigData,
|
||||
)
|
||||
|
|
@ -99,6 +110,107 @@ async def api_delete_machine(
|
|||
await delete_machine(machine_id)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DCA Clients (LPs) — scoped per (machine, user).
|
||||
# =============================================================================
|
||||
|
||||
|
||||
async def _machine_owned_by(machine_id: str, user_id: str) -> Machine:
|
||||
"""Lookup-with-ownership guard. 404 (not 403) so operators can't probe
|
||||
for other operators' machines."""
|
||||
machine = await get_machine(machine_id)
|
||||
if machine is None or machine.operator_user_id != user_id:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Machine not found")
|
||||
return machine
|
||||
|
||||
|
||||
async def _client_owned_by(client_id: str, user_id: str) -> DcaClient:
|
||||
"""Lookup-with-ownership guard for an LP record; ownership is checked
|
||||
transitively via the client's machine. 404 if either doesn't match."""
|
||||
client = await get_dca_client(client_id)
|
||||
if client is None:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Client not found")
|
||||
machine = await get_machine(client.machine_id)
|
||||
if machine is None or machine.operator_user_id != user_id:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Client not found")
|
||||
return client
|
||||
|
||||
|
||||
@satmachineadmin_api_router.post(
|
||||
"/api/v1/dca/clients", response_model=DcaClient
|
||||
)
|
||||
async def api_create_client(
|
||||
data: CreateDcaClientData, user: User = Depends(check_user_exists)
|
||||
) -> DcaClient:
|
||||
# Operator can only register LPs on machines they own.
|
||||
await _machine_owned_by(data.machine_id, user.id)
|
||||
return await create_dca_client(data)
|
||||
|
||||
|
||||
@satmachineadmin_api_router.get(
|
||||
"/api/v1/dca/clients", response_model=list[DcaClient]
|
||||
)
|
||||
async def api_list_clients(
|
||||
machine_id: str | None = None,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> list[DcaClient]:
|
||||
"""List the operator's LPs. Without ?machine_id, returns all LPs across
|
||||
the operator's fleet. With ?machine_id, scoped to that machine (with
|
||||
ownership check)."""
|
||||
if machine_id is None:
|
||||
return await get_dca_clients_for_operator(user.id)
|
||||
await _machine_owned_by(machine_id, user.id)
|
||||
return await get_dca_clients_for_machine(machine_id)
|
||||
|
||||
|
||||
@satmachineadmin_api_router.get(
|
||||
"/api/v1/dca/clients/{client_id}", response_model=DcaClient
|
||||
)
|
||||
async def api_get_client(
|
||||
client_id: str, user: User = Depends(check_user_exists)
|
||||
) -> DcaClient:
|
||||
return await _client_owned_by(client_id, user.id)
|
||||
|
||||
|
||||
@satmachineadmin_api_router.put(
|
||||
"/api/v1/dca/clients/{client_id}", response_model=DcaClient
|
||||
)
|
||||
async def api_update_client(
|
||||
client_id: str,
|
||||
data: UpdateDcaClientData,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> DcaClient:
|
||||
await _client_owned_by(client_id, user.id)
|
||||
updated = await update_dca_client(client_id, data)
|
||||
if updated is None:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Client not found")
|
||||
return updated
|
||||
|
||||
|
||||
@satmachineadmin_api_router.delete(
|
||||
"/api/v1/dca/clients/{client_id}", status_code=HTTPStatus.NO_CONTENT
|
||||
)
|
||||
async def api_delete_client(
|
||||
client_id: str, user: User = Depends(check_user_exists)
|
||||
) -> None:
|
||||
await _client_owned_by(client_id, user.id)
|
||||
await delete_dca_client(client_id)
|
||||
|
||||
|
||||
@satmachineadmin_api_router.get(
|
||||
"/api/v1/dca/clients/{client_id}/balance",
|
||||
response_model=ClientBalanceSummary,
|
||||
)
|
||||
async def api_get_client_balance(
|
||||
client_id: str, user: User = Depends(check_user_exists)
|
||||
) -> ClientBalanceSummary:
|
||||
await _client_owned_by(client_id, user.id)
|
||||
summary = await get_client_balance_summary(client_id)
|
||||
if summary is None:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Client not found")
|
||||
return summary
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Settlements (read-only at this phase; landing happens in tasks.py)
|
||||
# =============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue