feat(v2): operator-scoped API surface — machines, settlements, payments (P1b)
Replaces the views_api.py stub with the v1 operator-scoped REST surface
needed for the P1 frontend tasks (machine onboarding by npub, settlement
review, payment-leg audit). All endpoints filter on the authenticated
user's id so two operators on the same LNbits instance can never see
each other's data.
Endpoints (12 routes):
Machines (CRUD):
POST /api/v1/dca/machines — add by npub + wallet_id
GET /api/v1/dca/machines — operator's fleet
GET /api/v1/dca/machines/{id} — single (ownership check)
PUT /api/v1/dca/machines/{id} — update (ownership check)
DELETE /api/v1/dca/machines/{id} — delete (ownership check)
Settlements (read-only at this phase):
GET /api/v1/dca/settlements — operator-wide
GET /api/v1/dca/machines/{id}/settlements — per machine
GET /api/v1/dca/settlements/{id} — single (ownership check)
Payments (leg-typed audit):
GET /api/v1/dca/payments?leg_type=… — operator's payment legs
Super config (read-only here):
GET /api/v1/dca/super-config — operators read the
platform fee they pay
Catch-all:
/api/v1/dca/{...} → 503 with a precise message for not-yet-implemented
endpoints (clients, deposits, commission splits, partial-tx,
balance-settle, super-config write — all P2+).
All ownership checks live at the API boundary: if the route's resource
points to a machine the operator doesn't own, we 404 (not 403) so
operators can't probe for the existence of other operators' machines.
Verified routes register cleanly against LNbits 1.4 (nostr-transport).
22/22 calculation tests still green.
Refs: aiolabs/satmachineadmin#9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b91e49b642
commit
10b79ae900
1 changed files with 181 additions and 12 deletions
193
views_api.py
193
views_api.py
|
|
@ -1,20 +1,190 @@
|
|||
# Satoshi Machine v2 — API placeholder.
|
||||
# Satoshi Machine v2 — operator API surface (P1b).
|
||||
#
|
||||
# The v1 super-only Lamassu endpoints have been removed. The v2 operator-
|
||||
# scoped surface (machines / clients / deposits / settlements / commission
|
||||
# splits / partial-tx / balance-settle / super platform-fee) lands in P1+.
|
||||
# See plan section "Critical files to modify".
|
||||
#
|
||||
# This stub keeps __init__.py importable and surfaces a clear 503 on every
|
||||
# v1 route so existing clients get a precise error instead of a silent 404.
|
||||
# All endpoints are operator-scoped via check_user_exists. Every query
|
||||
# filters by the authenticated user's id so two operators on the same
|
||||
# LNbits instance can never see each other's machines, settlements, or
|
||||
# clients. The super-only platform-fee write endpoint lands in P2.
|
||||
|
||||
from http import HTTPStatus
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from lnbits.core.models import User
|
||||
from lnbits.decorators import check_user_exists
|
||||
|
||||
from .crud import (
|
||||
create_machine,
|
||||
delete_machine,
|
||||
get_machine,
|
||||
get_machines_for_operator,
|
||||
get_payments_for_operator,
|
||||
get_settlement,
|
||||
get_settlements_for_machine,
|
||||
get_settlements_for_operator,
|
||||
get_super_config,
|
||||
update_machine,
|
||||
)
|
||||
from .models import (
|
||||
CreateMachineData,
|
||||
DcaPayment,
|
||||
DcaSettlement,
|
||||
Machine,
|
||||
SuperConfig,
|
||||
UpdateMachineData,
|
||||
)
|
||||
|
||||
satmachineadmin_api_router = APIRouter()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Machines
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@satmachineadmin_api_router.post("/api/v1/dca/machines", response_model=Machine)
|
||||
async def api_create_machine(
|
||||
data: CreateMachineData, user: User = Depends(check_user_exists)
|
||||
) -> Machine:
|
||||
return await create_machine(user.id, data)
|
||||
|
||||
|
||||
@satmachineadmin_api_router.get(
|
||||
"/api/v1/dca/machines", response_model=list[Machine]
|
||||
)
|
||||
async def api_list_machines(
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> list[Machine]:
|
||||
return await get_machines_for_operator(user.id)
|
||||
|
||||
|
||||
@satmachineadmin_api_router.get(
|
||||
"/api/v1/dca/machines/{machine_id}", response_model=Machine
|
||||
)
|
||||
async def api_get_machine(
|
||||
machine_id: str, user: User = Depends(check_user_exists)
|
||||
) -> Machine:
|
||||
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
|
||||
|
||||
|
||||
@satmachineadmin_api_router.put(
|
||||
"/api/v1/dca/machines/{machine_id}", response_model=Machine
|
||||
)
|
||||
async def api_update_machine(
|
||||
machine_id: str,
|
||||
data: UpdateMachineData,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> Machine:
|
||||
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")
|
||||
updated = await update_machine(machine_id, data)
|
||||
if updated is None:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Machine not found")
|
||||
return updated
|
||||
|
||||
|
||||
@satmachineadmin_api_router.delete(
|
||||
"/api/v1/dca/machines/{machine_id}", status_code=HTTPStatus.NO_CONTENT
|
||||
)
|
||||
async def api_delete_machine(
|
||||
machine_id: str, user: User = Depends(check_user_exists)
|
||||
) -> None:
|
||||
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")
|
||||
await delete_machine(machine_id)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Settlements (read-only at this phase; landing happens in tasks.py)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@satmachineadmin_api_router.get(
|
||||
"/api/v1/dca/settlements", response_model=list[DcaSettlement]
|
||||
)
|
||||
async def api_list_settlements(
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> list[DcaSettlement]:
|
||||
return await get_settlements_for_operator(user.id)
|
||||
|
||||
|
||||
@satmachineadmin_api_router.get(
|
||||
"/api/v1/dca/machines/{machine_id}/settlements",
|
||||
response_model=list[DcaSettlement],
|
||||
)
|
||||
async def api_list_settlements_for_machine(
|
||||
machine_id: str, user: User = Depends(check_user_exists)
|
||||
) -> list[DcaSettlement]:
|
||||
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 await get_settlements_for_machine(machine_id)
|
||||
|
||||
|
||||
@satmachineadmin_api_router.get(
|
||||
"/api/v1/dca/settlements/{settlement_id}", response_model=DcaSettlement
|
||||
)
|
||||
async def api_get_settlement(
|
||||
settlement_id: str, user: User = Depends(check_user_exists)
|
||||
) -> DcaSettlement:
|
||||
settlement = await get_settlement(settlement_id)
|
||||
if settlement is None:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Settlement not found")
|
||||
machine = await get_machine(settlement.machine_id)
|
||||
if machine is None or machine.operator_user_id != user.id:
|
||||
raise HTTPException(HTTPStatus.NOT_FOUND, "Settlement not found")
|
||||
return settlement
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Payments (read-only — the leg-typed breakdown of distributions)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@satmachineadmin_api_router.get(
|
||||
"/api/v1/dca/payments", response_model=list[DcaPayment]
|
||||
)
|
||||
async def api_list_payments(
|
||||
leg_type: str | None = None,
|
||||
user: User = Depends(check_user_exists),
|
||||
) -> list[DcaPayment]:
|
||||
return await get_payments_for_operator(user.id, leg_type=leg_type)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Super config — read-only at this phase. Super-only write endpoint lands in P2.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@satmachineadmin_api_router.get(
|
||||
"/api/v1/dca/super-config", response_model=SuperConfig
|
||||
)
|
||||
async def api_get_super_config(
|
||||
_user: User = Depends(check_user_exists),
|
||||
) -> SuperConfig:
|
||||
"""Returns the platform-fee config so operators can display it as a
|
||||
read-only line item in their UI. The fee is set by the LNbits super
|
||||
instance-wide; operators see it but can't change it (write endpoint
|
||||
protected by check_super_user, landing in P2)."""
|
||||
config = await get_super_config()
|
||||
if config is None:
|
||||
raise HTTPException(
|
||||
HTTPStatus.NOT_FOUND, "Super config not initialised"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Catch-all stub for endpoints not yet implemented (clients, deposits,
|
||||
# commission splits, partial-tx, balance-settle, super-config write). Each
|
||||
# lands in a follow-up commit. The catch-all comes LAST so specific routes
|
||||
# above take precedence.
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@satmachineadmin_api_router.api_route(
|
||||
"/api/v1/dca/{full_path:path}",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
||||
|
|
@ -22,7 +192,6 @@ satmachineadmin_api_router = APIRouter()
|
|||
async def v2_in_progress_stub(full_path: str) -> None:
|
||||
raise HTTPException(
|
||||
HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
f"satmachineadmin v2 API not yet implemented (path: /{full_path}). "
|
||||
"The v1 Lamassu surface has been removed; per-operator endpoints "
|
||||
"land in P1. See plan.",
|
||||
f"satmachineadmin v2: /api/v1/dca/{full_path} not yet implemented "
|
||||
"(landing in P2+). See aiolabs/satmachineadmin#9.",
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue