Refactor DCA API endpoints to use superuser authentication: Updated all relevant DCA-related API endpoints to require check_super_user instead of require_admin_key, enhancing security. Adjusted client-side API calls to remove wallet admin key usage, ensuring session-based superuser authentication is utilized. Updated documentation in CLAUDE.md to reflect these changes.

This commit is contained in:
padreug 2025-06-26 13:18:01 +02:00
parent dfc2dd695c
commit 8871f24cec
4 changed files with 87 additions and 84 deletions

View file

@ -68,7 +68,8 @@ The Satoshi Machine Admin extension follows LNBits architecture patterns:
- Use `:attribute='value'` for binding, `v-html='value'` for HTML content - Use `:attribute='value'` for binding, `v-html='value'` for HTML content
2. **API Patterns**: 2. **API Patterns**:
- Always include wallet key (inkey/adminkey) as third parameter in API calls - Admin extension uses session-based superuser authentication (no API keys)
- Client extension uses wallet admin keys for user-specific operations
- Use `LNbits.api.request()` for all API calls - Use `LNbits.api.request()` for all API calls
- Destructure responses: `const {data} = await LNbits.api.request(...)` - Destructure responses: `const {data} = await LNbits.api.request(...)`
@ -79,10 +80,12 @@ The Satoshi Machine Admin extension follows LNBits architecture patterns:
### The Magical G Object ### The Magical G Object
The global `this.g` object provides access to: The global `this.g` object provides access to:
- `this.g.user` - Complete user data including wallets array - `this.g.user` - Complete user data including wallets array
- `this.g.user.wallets[0].inkey` - Invoice key for API calls - `this.g.user.wallets[0].inkey` - Invoice key (client extension only)
- `this.g.user.wallets[0].adminkey` - Admin key for privileged operations - `this.g.user.wallets[0].adminkey` - Admin key (client extension only)
- `this.g.wallet` - Currently selected wallet - `this.g.wallet` - Currently selected wallet
**Note**: Admin extension uses superuser session authentication, not wallet keys.
### Built-in Utilities ### Built-in Utilities
- Currency conversion: `/api/v1/currencies`, `/api/v1/conversion` - Currency conversion: `/api/v1/currencies`, `/api/v1/conversion`
- QR code generation: `/api/v1/qrcode/{data}` or Quasar VueQrcode component - QR code generation: `/api/v1/qrcode/{data}` or Quasar VueQrcode component
@ -208,9 +211,11 @@ commission_amount = 266800 - 258835 = 7,965 sats (to commission wallet)
- **Error Handling**: Graceful failure with detailed logging - **Error Handling**: Graceful failure with detailed logging
### Security Considerations ### Security Considerations
- **Superuser Authentication**: Admin extension requires LNBits superuser login
- **Wallet Admin Keys**: Client extension uses wallet admin keys for user operations
- **Database Access**: Only superusers can write to satoshimachine database
- SSH tunnel encryption for database connectivity - SSH tunnel encryption for database connectivity
- Read-only database permissions - Read-only database permissions for Lamassu access
- Wallet key validation for all financial operations
- Input sanitization and type validation - Input sanitization and type validation
- Audit logging for all administrative actions - Audit logging for all administrative actions

View file

@ -163,10 +163,10 @@ window.app = Vue.createApp({
// Configuration Methods // Configuration Methods
async getLamassuConfig() { async getLamassuConfig() {
try { try {
const {data} = await LNbits.api.request( const { data } = await LNbits.api.request(
'GET', 'GET',
'/satmachineadmin/api/v1/dca/config', '/satmachineadmin/api/v1/dca/config',
this.g.user.wallets[0].adminkey null
) )
this.lamassuConfig = data this.lamassuConfig = data
@ -208,10 +208,10 @@ window.app = Vue.createApp({
ssh_private_key: this.configDialog.data.ssh_private_key ssh_private_key: this.configDialog.data.ssh_private_key
} }
const {data: config} = await LNbits.api.request( const { data: config } = await LNbits.api.request(
'POST', 'POST',
'/satmachineadmin/api/v1/dca/config', '/satmachineadmin/api/v1/dca/config',
this.g.user.wallets[0].adminkey, null,
data data
) )
@ -254,7 +254,7 @@ window.app = Vue.createApp({
const { data } = await LNbits.api.request( const { data } = await LNbits.api.request(
'GET', 'GET',
'/satmachineadmin/api/v1/dca/clients', '/satmachineadmin/api/v1/dca/clients',
this.g.user.wallets[0].adminkey null
) )
// Fetch balance data for each client // Fetch balance data for each client
@ -264,7 +264,7 @@ window.app = Vue.createApp({
const { data: balance } = await LNbits.api.request( const { data: balance } = await LNbits.api.request(
'GET', 'GET',
`/satmachineadmin/api/v1/dca/clients/${client.id}/balance`, `/satmachineadmin/api/v1/dca/clients/${client.id}/balance`,
this.g.user.wallets[0].adminkey null
) )
return { return {
...client, ...client,
@ -300,7 +300,7 @@ window.app = Vue.createApp({
const { data: newDeposit } = await LNbits.api.request( const { data: newDeposit } = await LNbits.api.request(
'POST', 'POST',
'/satmachineadmin/api/v1/dca/deposits', '/satmachineadmin/api/v1/dca/deposits',
this.g.user.wallets[0].adminkey, null,
data data
) )
@ -328,7 +328,7 @@ window.app = Vue.createApp({
const { data: balance } = await LNbits.api.request( const { data: balance } = await LNbits.api.request(
'GET', 'GET',
`/satmachineadmin/api/v1/dca/clients/${client.id}/balance`, `/satmachineadmin/api/v1/dca/clients/${client.id}/balance`,
this.g.user.wallets[0].adminkey null
) )
this.clientDetailsDialog.data = client this.clientDetailsDialog.data = client
this.clientDetailsDialog.balance = balance this.clientDetailsDialog.balance = balance
@ -344,7 +344,7 @@ window.app = Vue.createApp({
const { data } = await LNbits.api.request( const { data } = await LNbits.api.request(
'GET', 'GET',
'/satmachineadmin/api/v1/dca/deposits', '/satmachineadmin/api/v1/dca/deposits',
this.g.user.wallets[0].adminkey null
) )
this.deposits = data this.deposits = data
} catch (error) { } catch (error) {
@ -375,7 +375,7 @@ window.app = Vue.createApp({
const { data: updatedDeposit } = await LNbits.api.request( const { data: updatedDeposit } = await LNbits.api.request(
'PUT', 'PUT',
`/satmachineadmin/api/v1/dca/deposits/${this.depositFormDialog.data.id}`, `/satmachineadmin/api/v1/dca/deposits/${this.depositFormDialog.data.id}`,
this.g.user.wallets[0].adminkey, null,
{ status: this.depositFormDialog.data.status, notes: data.notes } { status: this.depositFormDialog.data.status, notes: data.notes }
) )
const index = this.deposits.findIndex(d => d.id === updatedDeposit.id) const index = this.deposits.findIndex(d => d.id === updatedDeposit.id)
@ -387,7 +387,7 @@ window.app = Vue.createApp({
const { data: newDeposit } = await LNbits.api.request( const { data: newDeposit } = await LNbits.api.request(
'POST', 'POST',
'/satmachineadmin/api/v1/dca/deposits', '/satmachineadmin/api/v1/dca/deposits',
this.g.user.wallets[0].adminkey, null,
data data
) )
this.deposits.unshift(newDeposit) this.deposits.unshift(newDeposit)
@ -419,7 +419,7 @@ window.app = Vue.createApp({
const { data: updatedDeposit } = await LNbits.api.request( const { data: updatedDeposit } = await LNbits.api.request(
'PUT', 'PUT',
`/satmachineadmin/api/v1/dca/deposits/${deposit.id}/status`, `/satmachineadmin/api/v1/dca/deposits/${deposit.id}/status`,
this.g.user.wallets[0].adminkey, null,
{ status: 'confirmed', notes: 'Confirmed by admin - money placed in machine' } { status: 'confirmed', notes: 'Confirmed by admin - money placed in machine' }
) )
const index = this.deposits.findIndex(d => d.id === deposit.id) const index = this.deposits.findIndex(d => d.id === deposit.id)
@ -459,10 +459,10 @@ window.app = Vue.createApp({
async testDatabaseConnection() { async testDatabaseConnection() {
this.testingConnection = true this.testingConnection = true
try { try {
const {data} = await LNbits.api.request( const { data } = await LNbits.api.request(
'POST', 'POST',
'/satmachineadmin/api/v1/dca/test-connection', '/satmachineadmin/api/v1/dca/test-connection',
this.g.user.wallets[0].adminkey null
) )
// Show detailed results in a dialog // Show detailed results in a dialog
@ -505,10 +505,10 @@ window.app = Vue.createApp({
async manualPoll() { async manualPoll() {
this.runningManualPoll = true this.runningManualPoll = true
try { try {
const {data} = await LNbits.api.request( const { data } = await LNbits.api.request(
'POST', 'POST',
'/satmachineadmin/api/v1/dca/manual-poll', '/satmachineadmin/api/v1/dca/manual-poll',
this.g.user.wallets[0].adminkey null
) )
this.lastPollTime = new Date().toLocaleString() this.lastPollTime = new Date().toLocaleString()
@ -533,10 +533,10 @@ window.app = Vue.createApp({
async testTransaction() { async testTransaction() {
this.runningTestTransaction = true this.runningTestTransaction = true
try { try {
const {data} = await LNbits.api.request( const { data } = await LNbits.api.request(
'POST', 'POST',
'/satmachineadmin/api/v1/dca/test-transaction', '/satmachineadmin/api/v1/dca/test-transaction',
this.g.user.wallets[0].adminkey null
) )
// Show detailed results in a dialog // Show detailed results in a dialog
@ -589,7 +589,7 @@ window.app = Vue.createApp({
const { data } = await LNbits.api.request( const { data } = await LNbits.api.request(
'GET', 'GET',
'/satmachineadmin/api/v1/dca/transactions', '/satmachineadmin/api/v1/dca/transactions',
this.g.user.wallets[0].adminkey null
) )
this.lamassuTransactions = data this.lamassuTransactions = data
} catch (error) { } catch (error) {
@ -602,7 +602,7 @@ window.app = Vue.createApp({
const { data: distributions } = await LNbits.api.request( const { data: distributions } = await LNbits.api.request(
'GET', 'GET',
`/satmachineadmin/api/v1/dca/transactions/${transaction.id}/distributions`, `/satmachineadmin/api/v1/dca/transactions/${transaction.id}/distributions`,
this.g.user.wallets[0].adminkey null
) )
this.distributionDialog.transaction = transaction this.distributionDialog.transaction = transaction
@ -637,7 +637,7 @@ window.app = Vue.createApp({
// If SSH tunnel is enabled, validate SSH fields // If SSH tunnel is enabled, validate SSH fields
if (data.use_ssh_tunnel) { if (data.use_ssh_tunnel) {
const sshValid = data.ssh_host && data.ssh_username && const sshValid = data.ssh_host && data.ssh_username &&
(data.ssh_password || data.ssh_private_key) (data.ssh_password || data.ssh_private_key)
return basicValid && sshValid return basicValid && sshValid
} }

View file

@ -3,7 +3,7 @@
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from lnbits.core.models import User from lnbits.core.models import User
from lnbits.decorators import check_user_exists from lnbits.decorators import check_super_user
from lnbits.helpers import template_renderer from lnbits.helpers import template_renderer
satmachineadmin_generic_router = APIRouter() satmachineadmin_generic_router = APIRouter()
@ -13,9 +13,9 @@ def satmachineadmin_renderer():
return template_renderer(["satmachineadmin/templates"]) return template_renderer(["satmachineadmin/templates"])
# DCA Admin page # DCA Admin page - Requires superuser access
@satmachineadmin_generic_router.get("/", response_class=HTMLResponse) @satmachineadmin_generic_router.get("/", response_class=HTMLResponse)
async def index(req: Request, user: User = Depends(check_user_exists)): async def index(req: Request, user: User = Depends(check_super_user)):
return satmachineadmin_renderer().TemplateResponse( return satmachineadmin_renderer().TemplateResponse(
"satmachineadmin/index.html", {"request": req, "user": user.json()} "satmachineadmin/index.html", {"request": req, "user": user.json()}
) )

View file

@ -5,9 +5,9 @@ from typing import Optional
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from lnbits.core.crud import get_user from lnbits.core.crud import get_user
from lnbits.core.models import WalletTypeInfo from lnbits.core.models import User, WalletTypeInfo
from lnbits.core.services import create_invoice from lnbits.core.services import create_invoice
from lnbits.decorators import require_admin_key from lnbits.decorators import check_super_user
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from .crud import ( from .crud import (
@ -59,7 +59,7 @@ satmachineadmin_api_router = APIRouter()
@satmachineadmin_api_router.get("/api/v1/dca/clients") @satmachineadmin_api_router.get("/api/v1/dca/clients")
async def api_get_dca_clients( async def api_get_dca_clients(
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(check_super_user),
) -> list[DcaClient]: ) -> list[DcaClient]:
"""Get all DCA clients""" """Get all DCA clients"""
return await get_dca_clients() return await get_dca_clients()
@ -68,7 +68,7 @@ async def api_get_dca_clients(
@satmachineadmin_api_router.get("/api/v1/dca/clients/{client_id}") @satmachineadmin_api_router.get("/api/v1/dca/clients/{client_id}")
async def api_get_dca_client( async def api_get_dca_client(
client_id: str, client_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(check_super_user),
) -> DcaClient: ) -> DcaClient:
"""Get a specific DCA client""" """Get a specific DCA client"""
client = await get_dca_client(client_id) client = await get_dca_client(client_id)
@ -83,12 +83,10 @@ async def api_get_dca_client(
# Admin extension only reads existing clients and manages their deposits # Admin extension only reads existing clients and manages their deposits
@satmachineadmin_api_router.get("/api/v1/dca/clients/{client_id}/balance") @satmachineadmin_api_router.get("/api/v1/dca/clients/{client_id}/balance")
async def api_get_client_balance( async def api_get_client_balance(
client_id: str, client_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(check_super_user),
) -> ClientBalanceSummary: ) -> ClientBalanceSummary:
"""Get client balance summary""" """Get client balance summary"""
client = await get_dca_client(client_id) client = await get_dca_client(client_id)
@ -105,7 +103,7 @@ async def api_get_client_balance(
@satmachineadmin_api_router.get("/api/v1/dca/deposits") @satmachineadmin_api_router.get("/api/v1/dca/deposits")
async def api_get_deposits( async def api_get_deposits(
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(check_super_user),
) -> list[DcaDeposit]: ) -> list[DcaDeposit]:
"""Get all deposits""" """Get all deposits"""
return await get_all_deposits() return await get_all_deposits()
@ -114,7 +112,7 @@ async def api_get_deposits(
@satmachineadmin_api_router.get("/api/v1/dca/deposits/{deposit_id}") @satmachineadmin_api_router.get("/api/v1/dca/deposits/{deposit_id}")
async def api_get_deposit( async def api_get_deposit(
deposit_id: str, deposit_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(check_super_user),
) -> DcaDeposit: ) -> DcaDeposit:
"""Get a specific deposit""" """Get a specific deposit"""
deposit = await get_deposit(deposit_id) deposit = await get_deposit(deposit_id)
@ -128,7 +126,7 @@ async def api_get_deposit(
@satmachineadmin_api_router.post("/api/v1/dca/deposits", status_code=HTTPStatus.CREATED) @satmachineadmin_api_router.post("/api/v1/dca/deposits", status_code=HTTPStatus.CREATED)
async def api_create_deposit( async def api_create_deposit(
data: CreateDepositData, data: CreateDepositData,
wallet: WalletTypeInfo = Depends(require_admin_key), user: User = Depends(check_super_user),
) -> DcaDeposit: ) -> DcaDeposit:
"""Create a new deposit""" """Create a new deposit"""
# Verify client exists # Verify client exists
@ -145,7 +143,7 @@ async def api_create_deposit(
async def api_update_deposit_status( async def api_update_deposit_status(
deposit_id: str, deposit_id: str,
data: UpdateDepositStatusData, data: UpdateDepositStatusData,
wallet: WalletTypeInfo = Depends(require_admin_key), user: User = Depends(check_super_user),
) -> DcaDeposit: ) -> DcaDeposit:
"""Update deposit status (e.g., confirm deposit)""" """Update deposit status (e.g., confirm deposit)"""
deposit = await get_deposit(deposit_id) deposit = await get_deposit(deposit_id)
@ -168,7 +166,7 @@ async def api_update_deposit_status(
@satmachineadmin_api_router.post("/api/v1/dca/test-connection") @satmachineadmin_api_router.post("/api/v1/dca/test-connection")
async def api_test_database_connection( async def api_test_database_connection(
wallet: WalletTypeInfo = Depends(require_admin_key), user: User = Depends(check_super_user),
): ):
"""Test connection to Lamassu database with detailed reporting""" """Test connection to Lamassu database with detailed reporting"""
try: try:
@ -191,7 +189,7 @@ async def api_test_database_connection(
@satmachineadmin_api_router.post("/api/v1/dca/manual-poll") @satmachineadmin_api_router.post("/api/v1/dca/manual-poll")
async def api_manual_poll( async def api_manual_poll(
wallet: WalletTypeInfo = Depends(require_admin_key), user: User = Depends(check_super_user),
): ):
"""Manually trigger a poll of the Lamassu database""" """Manually trigger a poll of the Lamassu database"""
try: try:
@ -237,7 +235,7 @@ async def api_manual_poll(
@satmachineadmin_api_router.post("/api/v1/dca/test-transaction") @satmachineadmin_api_router.post("/api/v1/dca/test-transaction")
async def api_test_transaction( async def api_test_transaction(
wallet: WalletTypeInfo = Depends(require_admin_key), user: User = Depends(check_super_user),
crypto_atoms: int = 103, crypto_atoms: int = 103,
commission_percentage: float = 0.03, commission_percentage: float = 0.03,
discount: float = 0.0, discount: float = 0.0,
@ -303,7 +301,7 @@ async def api_test_transaction(
@satmachineadmin_api_router.get("/api/v1/dca/transactions") @satmachineadmin_api_router.get("/api/v1/dca/transactions")
async def api_get_lamassu_transactions( async def api_get_lamassu_transactions(
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(check_super_user),
) -> list[StoredLamassuTransaction]: ) -> list[StoredLamassuTransaction]:
"""Get all processed Lamassu transactions""" """Get all processed Lamassu transactions"""
return await get_all_lamassu_transactions() return await get_all_lamassu_transactions()
@ -312,7 +310,7 @@ async def api_get_lamassu_transactions(
@satmachineadmin_api_router.get("/api/v1/dca/transactions/{transaction_id}") @satmachineadmin_api_router.get("/api/v1/dca/transactions/{transaction_id}")
async def api_get_lamassu_transaction( async def api_get_lamassu_transaction(
transaction_id: str, transaction_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(check_super_user),
) -> StoredLamassuTransaction: ) -> StoredLamassuTransaction:
"""Get a specific Lamassu transaction with details""" """Get a specific Lamassu transaction with details"""
transaction = await get_lamassu_transaction(transaction_id) transaction = await get_lamassu_transaction(transaction_id)
@ -328,7 +326,7 @@ async def api_get_lamassu_transaction(
) )
async def api_get_transaction_distributions( async def api_get_transaction_distributions(
transaction_id: str, transaction_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(check_super_user),
) -> list[dict]: ) -> list[dict]:
"""Get distribution details for a specific Lamassu transaction""" """Get distribution details for a specific Lamassu transaction"""
# Get the stored transaction # Get the stored transaction
@ -371,7 +369,7 @@ async def api_get_transaction_distributions(
@satmachineadmin_api_router.get("/api/v1/dca/config") @satmachineadmin_api_router.get("/api/v1/dca/config")
async def api_get_lamassu_config( async def api_get_lamassu_config(
wallet: WalletTypeInfo = Depends(require_admin_key), wallet: WalletTypeInfo = Depends(check_super_user),
) -> Optional[LamassuConfig]: ) -> Optional[LamassuConfig]:
"""Get active Lamassu database configuration""" """Get active Lamassu database configuration"""
return await get_active_lamassu_config() return await get_active_lamassu_config()
@ -380,7 +378,7 @@ async def api_get_lamassu_config(
@satmachineadmin_api_router.post("/api/v1/dca/config", status_code=HTTPStatus.CREATED) @satmachineadmin_api_router.post("/api/v1/dca/config", status_code=HTTPStatus.CREATED)
async def api_create_lamassu_config( async def api_create_lamassu_config(
data: CreateLamassuConfigData, data: CreateLamassuConfigData,
wallet: WalletTypeInfo = Depends(require_admin_key), user: User = Depends(check_super_user),
) -> LamassuConfig: ) -> LamassuConfig:
"""Create/update Lamassu database configuration""" """Create/update Lamassu database configuration"""
return await create_lamassu_config(data) return await create_lamassu_config(data)
@ -390,7 +388,7 @@ async def api_create_lamassu_config(
async def api_update_lamassu_config( async def api_update_lamassu_config(
config_id: str, config_id: str,
data: UpdateLamassuConfigData, data: UpdateLamassuConfigData,
wallet: WalletTypeInfo = Depends(require_admin_key), user: User = Depends(check_super_user),
) -> LamassuConfig: ) -> LamassuConfig:
"""Update Lamassu database configuration""" """Update Lamassu database configuration"""
config = await get_lamassu_config(config_id) config = await get_lamassu_config(config_id)
@ -411,7 +409,7 @@ async def api_update_lamassu_config(
@satmachineadmin_api_router.delete("/api/v1/dca/config/{config_id}") @satmachineadmin_api_router.delete("/api/v1/dca/config/{config_id}")
async def api_delete_lamassu_config( async def api_delete_lamassu_config(
config_id: str, config_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key), user: User = Depends(check_super_user),
): ):
"""Delete Lamassu database configuration""" """Delete Lamassu database configuration"""
config = await get_lamassu_config(config_id) config = await get_lamassu_config(config_id)