diff --git a/crud.py b/crud.py index d753cc4..847da5c 100644 --- a/crud.py +++ b/crud.py @@ -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 diff --git a/migrations.py b/migrations.py index 4a13dad..0c8b0ba 100644 --- a/migrations.py +++ b/migrations.py @@ -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} + ); + """ + ) diff --git a/models.py b/models.py index 185d72e..e54960a 100644 --- a/models.py +++ b/models.py @@ -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 diff --git a/services.py b/services.py new file mode 100644 index 0000000..ff3e5d6 --- /dev/null +++ b/services.py @@ -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 diff --git a/static/js/index.js b/static/js/index.js index ade58c9..3ecdabe 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -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() } }) diff --git a/templates/castle/index.html b/templates/castle/index.html index d54d21d..4878b80 100644 --- a/templates/castle/index.html +++ b/templates/castle/index.html @@ -11,8 +11,17 @@
-
🏰 Castle Accounting
-

Track expenses, receivables, and balances for the collective

+
+
+
🏰 Castle Accounting
+

Track expenses, receivables, and balances for the collective

+
+
+ + Settings + +
+
@@ -233,4 +242,32 @@ + + + + +
Castle Settings
+ + + +
+ This wallet will be used for Castle operations and transactions. +
+ +
+ + Save Settings + + Cancel +
+
+
+
+ {% endblock %} diff --git a/views_api.py b/views_api.py index ceacbb5..7db76d9 100644 --- a/views_api.py +++ b/views_api.py @@ -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)