Adds user settings for the Castle extension

Adds functionality to configure the Castle extension, including a wallet ID.

This allows administrators to customize the extension's behavior by specifying a dedicated wallet for castle operations.
This commit is contained in:
padreug 2025-10-22 13:55:52 +02:00
parent ceabf96f79
commit 29983cedb7
7 changed files with 199 additions and 2 deletions

32
crud.py
View file

@ -8,12 +8,14 @@ from lnbits.helpers import urlsafe_short_hash
from .models import (
Account,
AccountType,
CastleSettings,
CreateAccount,
CreateEntryLine,
CreateJournalEntry,
EntryLine,
JournalEntry,
UserBalance,
UserCastleSettings,
)
db = Database("ext_castle")
@ -345,3 +347,33 @@ async def get_account_transactions(
transactions.append((entry, line))
return transactions
# ===== SETTINGS =====
async def create_castle_settings(
user_id: str, data: CastleSettings
) -> CastleSettings:
settings = UserCastleSettings(**data.dict(), id=user_id)
await db.insert("extension_settings", settings)
return settings
async def get_castle_settings(user_id: str) -> Optional[CastleSettings]:
return await db.fetchone(
"""
SELECT * FROM extension_settings
WHERE id = :user_id
""",
{"user_id": user_id},
CastleSettings,
)
async def update_castle_settings(
user_id: str, data: CastleSettings
) -> CastleSettings:
settings = UserCastleSettings(**data.dict(), id=user_id)
await db.update("extension_settings", settings)
return settings

View file

@ -125,3 +125,18 @@ async def m002_add_metadata_column(db):
ALTER TABLE entry_lines ADD COLUMN metadata TEXT DEFAULT '{}';
"""
)
async def m003_extension_settings(db):
"""
Create extension_settings table for Castle configuration.
"""
await db.execute(
f"""
CREATE TABLE extension_settings (
id TEXT NOT NULL PRIMARY KEY,
castle_wallet_id TEXT,
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)

View file

@ -102,3 +102,20 @@ class RevenueEntry(BaseModel):
payment_method_account: str # e.g., "Cash", "Bank", "Lightning"
reference: Optional[str] = None
currency: Optional[str] = None # If None, amount is in satoshis. Otherwise, fiat currency code
class CastleSettings(BaseModel):
"""Settings for the Castle extension"""
castle_wallet_id: Optional[str] = None # The wallet ID that represents the Castle
updated_at: datetime = Field(default_factory=lambda: datetime.now())
@classmethod
def is_admin_only(cls) -> bool:
return True
class UserCastleSettings(CastleSettings):
"""User-specific settings (stored with user_id)"""
id: str

23
services.py Normal file
View file

@ -0,0 +1,23 @@
from .crud import (
create_castle_settings,
get_castle_settings,
update_castle_settings,
)
from .models import CastleSettings
async def get_settings(user_id: str) -> CastleSettings:
settings = await get_castle_settings(user_id)
if not settings:
settings = await create_castle_settings(user_id, CastleSettings())
return settings
async def update_settings(user_id: str, data: CastleSettings) -> CastleSettings:
settings = await get_castle_settings(user_id)
if not settings:
settings = await create_castle_settings(user_id, data)
else:
settings = await update_castle_settings(user_id, data)
return settings

View file

@ -11,6 +11,8 @@ window.app = Vue.createApp({
transactions: [],
accounts: [],
currencies: [],
settings: null,
isAdmin: false,
expenseDialog: {
show: false,
description: '',
@ -25,6 +27,11 @@ window.app = Vue.createApp({
show: false,
amount: null,
loading: false
},
settingsDialog: {
show: false,
castleWalletId: '',
loading: false
}
}
},
@ -95,6 +102,47 @@ window.app = Vue.createApp({
LNbits.utils.notifyApiError(error)
}
},
async loadSettings() {
try {
const response = await LNbits.api.request(
'GET',
'/castle/api/v1/settings',
this.g.user.wallets[0].adminkey
)
this.settings = response.data
this.isAdmin = true
} catch (error) {
// Not admin or settings not available
this.isAdmin = false
}
},
showSettingsDialog() {
this.settingsDialog.castleWalletId = this.settings?.castle_wallet_id || ''
this.settingsDialog.show = true
},
async submitSettings() {
this.settingsDialog.loading = true
try {
await LNbits.api.request(
'PUT',
'/castle/api/v1/settings',
this.g.user.wallets[0].adminkey,
{
castle_wallet_id: this.settingsDialog.castleWalletId
}
)
this.$q.notify({
type: 'positive',
message: 'Settings updated successfully'
})
this.settingsDialog.show = false
await this.loadSettings()
} catch (error) {
LNbits.utils.notifyApiError(error)
} finally {
this.settingsDialog.loading = false
}
},
async submitExpense() {
this.expenseDialog.loading = true
try {
@ -188,5 +236,6 @@ window.app = Vue.createApp({
await this.loadTransactions()
await this.loadAccounts()
await this.loadCurrencies()
await this.loadSettings()
}
})

View file

@ -11,8 +11,17 @@
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<q-card>
<q-card-section>
<h5 class="q-my-none">🏰 Castle Accounting</h5>
<p>Track expenses, receivables, and balances for the collective</p>
<div class="row items-center no-wrap">
<div class="col">
<h5 class="q-my-none">🏰 Castle Accounting</h5>
<p class="q-mb-none">Track expenses, receivables, and balances for the collective</p>
</div>
<div class="col-auto" v-if="isAdmin">
<q-btn flat round icon="settings" @click="showSettingsDialog">
<q-tooltip>Settings</q-tooltip>
</q-btn>
</div>
</div>
</q-card-section>
</q-card>
@ -233,4 +242,32 @@
</q-card>
</q-dialog>
<!-- Settings Dialog -->
<q-dialog v-model="settingsDialog.show" position="top">
<q-card v-if="settingsDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card">
<q-form @submit="submitSettings" class="q-gutter-md">
<div class="text-h6 q-mb-md">Castle Settings</div>
<q-input
filled
dense
v-model.trim="settingsDialog.castleWalletId"
label="Castle Wallet ID *"
placeholder="The wallet ID that represents the Castle"
></q-input>
<div class="text-caption text-grey">
This wallet will be used for Castle operations and transactions.
</div>
<div class="row q-mt-lg">
<q-btn unelevated color="primary" type="submit" :loading="settingsDialog.loading">
Save Settings
</q-btn>
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Cancel</q-btn>
</div>
</q-form>
</q-card>
</q-dialog>
{% endblock %}

View file

@ -22,6 +22,7 @@ from .crud import (
from .models import (
Account,
AccountType,
CastleSettings,
CreateAccount,
CreateEntryLine,
CreateJournalEntry,
@ -31,6 +32,7 @@ from .models import (
RevenueEntry,
UserBalance,
)
from .services import get_settings, update_settings
castle_api_router = APIRouter()
@ -450,3 +452,25 @@ async def api_pay_user(
"new_balance": balance.balance,
"message": "Payment recorded successfully",
}
# ===== SETTINGS ENDPOINTS =====
@castle_api_router.get("/api/v1/settings")
async def api_get_settings(
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> CastleSettings:
"""Get Castle settings (admin only)"""
user_id = "admin"
return await get_settings(user_id)
@castle_api_router.put("/api/v1/settings")
async def api_update_settings(
data: CastleSettings,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> CastleSettings:
"""Update Castle settings (admin only)"""
user_id = "admin"
return await update_settings(user_id, data)