From 8871f24cec9f942c30b299f9b18a7e0513a4f2ab Mon Sep 17 00:00:00 2001 From: padreug Date: Thu, 26 Jun 2025 13:18:01 +0200 Subject: [PATCH] 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. --- CLAUDE.md | 15 ++++--- static/js/index.js | 110 ++++++++++++++++++++++----------------------- views.py | 6 +-- views_api.py | 40 ++++++++--------- 4 files changed, 87 insertions(+), 84 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7898dee..9d2af64 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,7 +68,8 @@ The Satoshi Machine Admin extension follows LNBits architecture patterns: - Use `:attribute='value'` for binding, `v-html='value'` for HTML content 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 - 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 global `this.g` object provides access to: - `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].adminkey` - Admin key for privileged operations +- `this.g.user.wallets[0].inkey` - Invoice key (client extension only) +- `this.g.user.wallets[0].adminkey` - Admin key (client extension only) - `this.g.wallet` - Currently selected wallet +**Note**: Admin extension uses superuser session authentication, not wallet keys. + ### Built-in Utilities - Currency conversion: `/api/v1/currencies`, `/api/v1/conversion` - 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 ### 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 -- Read-only database permissions -- Wallet key validation for all financial operations +- Read-only database permissions for Lamassu access - Input sanitization and type validation - Audit logging for all administrative actions diff --git a/static/js/index.js b/static/js/index.js index 684541f..e246d7e 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -86,14 +86,14 @@ window.app = Vue.createApp({ amount: null, notes: '' }, - + // Polling status lastPollTime: null, testingConnection: false, runningManualPoll: false, runningTestTransaction: false, lamassuConfig: null, - + // Config dialog configDialog: { show: false, @@ -163,13 +163,13 @@ window.app = Vue.createApp({ // Configuration Methods async getLamassuConfig() { try { - const {data} = await LNbits.api.request( + const { data } = await LNbits.api.request( 'GET', '/satmachineadmin/api/v1/dca/config', - this.g.user.wallets[0].adminkey + null ) this.lamassuConfig = data - + // When opening config dialog, populate the selected wallets if they exist if (data && data.source_wallet_id) { const wallet = this.g.user.wallets.find(w => w.id === data.source_wallet_id) @@ -188,7 +188,7 @@ window.app = Vue.createApp({ this.lamassuConfig = null } }, - + async saveConfiguration() { try { const data = { @@ -207,17 +207,17 @@ window.app = Vue.createApp({ ssh_password: this.configDialog.data.ssh_password, ssh_private_key: this.configDialog.data.ssh_private_key } - - const {data: config} = await LNbits.api.request( + + const { data: config } = await LNbits.api.request( 'POST', '/satmachineadmin/api/v1/dca/config', - this.g.user.wallets[0].adminkey, + null, data ) - + this.lamassuConfig = config this.closeConfigDialog() - + this.$q.notify({ type: 'positive', message: 'Database configuration saved successfully', @@ -227,7 +227,7 @@ window.app = Vue.createApp({ LNbits.utils.notifyApiError(error) } }, - + closeConfigDialog() { this.configDialog.show = false this.configDialog.data = { @@ -254,9 +254,9 @@ window.app = Vue.createApp({ const { data } = await LNbits.api.request( 'GET', '/satmachineadmin/api/v1/dca/clients', - this.g.user.wallets[0].adminkey + null ) - + // Fetch balance data for each client const clientsWithBalances = await Promise.all( data.map(async (client) => { @@ -264,7 +264,7 @@ window.app = Vue.createApp({ const { data: balance } = await LNbits.api.request( 'GET', `/satmachineadmin/api/v1/dca/clients/${client.id}/balance`, - this.g.user.wallets[0].adminkey + null ) return { ...client, @@ -279,7 +279,7 @@ window.app = Vue.createApp({ } }) ) - + this.dcaClients = clientsWithBalances } catch (error) { LNbits.utils.notifyApiError(error) @@ -300,7 +300,7 @@ window.app = Vue.createApp({ const { data: newDeposit } = await LNbits.api.request( 'POST', '/satmachineadmin/api/v1/dca/deposits', - this.g.user.wallets[0].adminkey, + null, data ) @@ -328,7 +328,7 @@ window.app = Vue.createApp({ const { data: balance } = await LNbits.api.request( 'GET', `/satmachineadmin/api/v1/dca/clients/${client.id}/balance`, - this.g.user.wallets[0].adminkey + null ) this.clientDetailsDialog.data = client this.clientDetailsDialog.balance = balance @@ -344,7 +344,7 @@ window.app = Vue.createApp({ const { data } = await LNbits.api.request( 'GET', '/satmachineadmin/api/v1/dca/deposits', - this.g.user.wallets[0].adminkey + null ) this.deposits = data } catch (error) { @@ -375,7 +375,7 @@ window.app = Vue.createApp({ const { data: updatedDeposit } = await LNbits.api.request( 'PUT', `/satmachineadmin/api/v1/dca/deposits/${this.depositFormDialog.data.id}`, - this.g.user.wallets[0].adminkey, + null, { status: this.depositFormDialog.data.status, notes: data.notes } ) 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( 'POST', '/satmachineadmin/api/v1/dca/deposits', - this.g.user.wallets[0].adminkey, + null, data ) this.deposits.unshift(newDeposit) @@ -419,7 +419,7 @@ window.app = Vue.createApp({ const { data: updatedDeposit } = await LNbits.api.request( 'PUT', `/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' } ) const index = this.deposits.findIndex(d => d.id === deposit.id) @@ -454,30 +454,30 @@ window.app = Vue.createApp({ async exportLamassuTransactionsCSV() { await LNbits.utils.exportCSV(this.lamassuTransactionsTable.columns, this.lamassuTransactions) }, - + // Polling Methods async testDatabaseConnection() { this.testingConnection = true try { - const {data} = await LNbits.api.request( + const { data } = await LNbits.api.request( 'POST', '/satmachineadmin/api/v1/dca/test-connection', - this.g.user.wallets[0].adminkey + null ) - + // Show detailed results in a dialog const stepsList = data.steps ? data.steps.join('\n') : 'No detailed steps available' - + let dialogContent = `Connection Test Results

` - + if (data.ssh_tunnel_used) { dialogContent += `SSH Tunnel: ${data.ssh_tunnel_success ? '✅ Success' : '❌ Failed'}
` } - + dialogContent += `Database: ${data.database_connection_success ? '✅ Success' : '❌ Failed'}

` dialogContent += `Detailed Steps:
` dialogContent += stepsList.replace(/\n/g, '
') - + this.$q.dialog({ title: data.success ? 'Connection Test Passed' : 'Connection Test Failed', message: dialogContent, @@ -487,37 +487,37 @@ window.app = Vue.createApp({ label: 'Close' } }) - + // Also show a brief notification this.$q.notify({ type: data.success ? 'positive' : 'negative', message: data.message, timeout: 3000 }) - + } catch (error) { LNbits.utils.notifyApiError(error) } finally { this.testingConnection = false } }, - + async manualPoll() { this.runningManualPoll = true try { - const {data} = await LNbits.api.request( + const { data } = await LNbits.api.request( 'POST', '/satmachineadmin/api/v1/dca/manual-poll', - this.g.user.wallets[0].adminkey + null ) - + this.lastPollTime = new Date().toLocaleString() this.$q.notify({ type: 'positive', message: `Manual poll completed. Found ${data.transactions_processed} new transactions.`, timeout: 5000 }) - + // Refresh data await this.getDcaClients() // Refresh to show updated balances await this.getDeposits() @@ -529,19 +529,19 @@ window.app = Vue.createApp({ this.runningManualPoll = false } }, - + async testTransaction() { this.runningTestTransaction = true try { - const {data} = await LNbits.api.request( + const { data } = await LNbits.api.request( 'POST', '/satmachineadmin/api/v1/dca/test-transaction', - this.g.user.wallets[0].adminkey + null ) - + // Show detailed results in a dialog const details = data.transaction_details - + let dialogContent = `Test Transaction Results

` dialogContent += `Transaction ID: ${details.transaction_id}
` dialogContent += `Total Amount: ${details.total_amount_sats} sats
` @@ -552,7 +552,7 @@ window.app = Vue.createApp({ dialogContent += `Effective Commission: ${details.effective_commission}%
` } dialogContent += `
Check your wallets to see the distributions!` - + this.$q.dialog({ title: 'Test Transaction Completed', message: dialogContent, @@ -562,20 +562,20 @@ window.app = Vue.createApp({ label: 'Great!' } }) - + // Also show a brief notification this.$q.notify({ type: 'positive', message: `Test transaction processed: ${details.total_amount_sats} sats distributed`, timeout: 5000 }) - + // Refresh data await this.getDcaClients() // Refresh to show updated balances await this.getDeposits() await this.getLamassuTransactions() await this.getLamassuConfig() - + } catch (error) { LNbits.utils.notifyApiError(error) } finally { @@ -589,7 +589,7 @@ window.app = Vue.createApp({ const { data } = await LNbits.api.request( 'GET', '/satmachineadmin/api/v1/dca/transactions', - this.g.user.wallets[0].adminkey + null ) this.lamassuTransactions = data } catch (error) { @@ -602,9 +602,9 @@ window.app = Vue.createApp({ const { data: distributions } = await LNbits.api.request( 'GET', `/satmachineadmin/api/v1/dca/transactions/${transaction.id}/distributions`, - this.g.user.wallets[0].adminkey + null ) - + this.distributionDialog.transaction = transaction this.distributionDialog.distributions = distributions this.distributionDialog.show = true @@ -630,20 +630,20 @@ window.app = Vue.createApp({ computed: { isConfigFormValid() { const data = this.configDialog.data - + // Basic database fields are required const basicValid = data.host && data.database_name && data.username && data.selectedWallet - + // If SSH tunnel is enabled, validate SSH fields if (data.use_ssh_tunnel) { - const sshValid = data.ssh_host && data.ssh_username && - (data.ssh_password || data.ssh_private_key) + const sshValid = data.ssh_host && data.ssh_username && + (data.ssh_password || data.ssh_private_key) return basicValid && sshValid } - + return basicValid }, - + clientOptions() { return this.dcaClients.map(client => ({ label: `${client.username || client.user_id.substring(0, 8) + '...'} (${client.dca_mode})`, diff --git a/views.py b/views.py index 28b93c9..61701cd 100644 --- a/views.py +++ b/views.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse 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 satmachineadmin_generic_router = APIRouter() @@ -13,9 +13,9 @@ def satmachineadmin_renderer(): return template_renderer(["satmachineadmin/templates"]) -# DCA Admin page +# DCA Admin page - Requires superuser access @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( "satmachineadmin/index.html", {"request": req, "user": user.json()} ) diff --git a/views_api.py b/views_api.py index da143ed..df1bbc0 100644 --- a/views_api.py +++ b/views_api.py @@ -5,9 +5,9 @@ from typing import Optional from fastapi import APIRouter, Depends, Request 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.decorators import require_admin_key +from lnbits.decorators import check_super_user from starlette.exceptions import HTTPException from .crud import ( @@ -59,7 +59,7 @@ satmachineadmin_api_router = APIRouter() @satmachineadmin_api_router.get("/api/v1/dca/clients") async def api_get_dca_clients( - wallet: WalletTypeInfo = Depends(require_admin_key), + wallet: WalletTypeInfo = Depends(check_super_user), ) -> list[DcaClient]: """Get all 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}") async def api_get_dca_client( client_id: str, - wallet: WalletTypeInfo = Depends(require_admin_key), + wallet: WalletTypeInfo = Depends(check_super_user), ) -> DcaClient: """Get a specific DCA client""" 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 - - @satmachineadmin_api_router.get("/api/v1/dca/clients/{client_id}/balance") async def api_get_client_balance( client_id: str, - wallet: WalletTypeInfo = Depends(require_admin_key), + wallet: WalletTypeInfo = Depends(check_super_user), ) -> ClientBalanceSummary: """Get client balance summary""" 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") async def api_get_deposits( - wallet: WalletTypeInfo = Depends(require_admin_key), + wallet: WalletTypeInfo = Depends(check_super_user), ) -> list[DcaDeposit]: """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}") async def api_get_deposit( deposit_id: str, - wallet: WalletTypeInfo = Depends(require_admin_key), + wallet: WalletTypeInfo = Depends(check_super_user), ) -> DcaDeposit: """Get a specific deposit""" 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) async def api_create_deposit( data: CreateDepositData, - wallet: WalletTypeInfo = Depends(require_admin_key), + user: User = Depends(check_super_user), ) -> DcaDeposit: """Create a new deposit""" # Verify client exists @@ -145,7 +143,7 @@ async def api_create_deposit( async def api_update_deposit_status( deposit_id: str, data: UpdateDepositStatusData, - wallet: WalletTypeInfo = Depends(require_admin_key), + user: User = Depends(check_super_user), ) -> DcaDeposit: """Update deposit status (e.g., confirm deposit)""" 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") 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""" try: @@ -191,7 +189,7 @@ async def api_test_database_connection( @satmachineadmin_api_router.post("/api/v1/dca/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""" try: @@ -237,7 +235,7 @@ async def api_manual_poll( @satmachineadmin_api_router.post("/api/v1/dca/test-transaction") async def api_test_transaction( - wallet: WalletTypeInfo = Depends(require_admin_key), + user: User = Depends(check_super_user), crypto_atoms: int = 103, commission_percentage: float = 0.03, discount: float = 0.0, @@ -303,7 +301,7 @@ async def api_test_transaction( @satmachineadmin_api_router.get("/api/v1/dca/transactions") async def api_get_lamassu_transactions( - wallet: WalletTypeInfo = Depends(require_admin_key), + wallet: WalletTypeInfo = Depends(check_super_user), ) -> list[StoredLamassuTransaction]: """Get all processed 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}") async def api_get_lamassu_transaction( transaction_id: str, - wallet: WalletTypeInfo = Depends(require_admin_key), + wallet: WalletTypeInfo = Depends(check_super_user), ) -> StoredLamassuTransaction: """Get a specific Lamassu transaction with details""" transaction = await get_lamassu_transaction(transaction_id) @@ -328,7 +326,7 @@ async def api_get_lamassu_transaction( ) async def api_get_transaction_distributions( transaction_id: str, - wallet: WalletTypeInfo = Depends(require_admin_key), + wallet: WalletTypeInfo = Depends(check_super_user), ) -> list[dict]: """Get distribution details for a specific Lamassu transaction""" # Get the stored transaction @@ -371,7 +369,7 @@ async def api_get_transaction_distributions( @satmachineadmin_api_router.get("/api/v1/dca/config") async def api_get_lamassu_config( - wallet: WalletTypeInfo = Depends(require_admin_key), + wallet: WalletTypeInfo = Depends(check_super_user), ) -> Optional[LamassuConfig]: """Get active Lamassu database configuration""" 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) async def api_create_lamassu_config( data: CreateLamassuConfigData, - wallet: WalletTypeInfo = Depends(require_admin_key), + user: User = Depends(check_super_user), ) -> LamassuConfig: """Create/update Lamassu database configuration""" return await create_lamassu_config(data) @@ -390,7 +388,7 @@ async def api_create_lamassu_config( async def api_update_lamassu_config( config_id: str, data: UpdateLamassuConfigData, - wallet: WalletTypeInfo = Depends(require_admin_key), + user: User = Depends(check_super_user), ) -> LamassuConfig: """Update Lamassu database configuration""" 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}") async def api_delete_lamassu_config( config_id: str, - wallet: WalletTypeInfo = Depends(require_admin_key), + user: User = Depends(check_super_user), ): """Delete Lamassu database configuration""" config = await get_lamassu_config(config_id)