diff --git a/crud.py b/crud.py index 847da5c..85ba9cc 100644 --- a/crud.py +++ b/crud.py @@ -14,8 +14,10 @@ from .models import ( CreateJournalEntry, EntryLine, JournalEntry, + StoredUserWalletSettings, UserBalance, UserCastleSettings, + UserWalletSettings, ) db = Database("ext_castle") @@ -377,3 +379,33 @@ async def update_castle_settings( settings = UserCastleSettings(**data.dict(), id=user_id) await db.update("extension_settings", settings) return settings + + +# ===== USER WALLET SETTINGS ===== + + +async def create_user_wallet_settings( + user_id: str, data: UserWalletSettings +) -> UserWalletSettings: + settings = StoredUserWalletSettings(**data.dict(), id=user_id) + await db.insert("user_wallet_settings", settings) + return settings + + +async def get_user_wallet_settings(user_id: str) -> Optional[UserWalletSettings]: + return await db.fetchone( + """ + SELECT * FROM user_wallet_settings + WHERE id = :user_id + """, + {"user_id": user_id}, + UserWalletSettings, + ) + + +async def update_user_wallet_settings( + user_id: str, data: UserWalletSettings +) -> UserWalletSettings: + settings = StoredUserWalletSettings(**data.dict(), id=user_id) + await db.update("user_wallet_settings", settings) + return settings diff --git a/migrations.py b/migrations.py index 0c8b0ba..2144a8e 100644 --- a/migrations.py +++ b/migrations.py @@ -140,3 +140,18 @@ async def m003_extension_settings(db): ); """ ) + + +async def m004_user_wallet_settings(db): + """ + Create user_wallet_settings table for per-user wallet configuration. + """ + await db.execute( + f""" + CREATE TABLE user_wallet_settings ( + id TEXT NOT NULL PRIMARY KEY, + user_wallet_id TEXT, + updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + ); + """ + ) diff --git a/models.py b/models.py index e54960a..f1aa8e6 100644 --- a/models.py +++ b/models.py @@ -119,3 +119,16 @@ class UserCastleSettings(CastleSettings): """User-specific settings (stored with user_id)""" id: str + + +class UserWalletSettings(BaseModel): + """Per-user wallet settings""" + + user_wallet_id: Optional[str] = None # The wallet ID for this specific user + updated_at: datetime = Field(default_factory=lambda: datetime.now()) + + +class StoredUserWalletSettings(UserWalletSettings): + """Stored user wallet settings with user ID""" + + id: str # user_id diff --git a/services.py b/services.py index ff3e5d6..47a3d7b 100644 --- a/services.py +++ b/services.py @@ -1,9 +1,12 @@ from .crud import ( create_castle_settings, + create_user_wallet_settings, get_castle_settings, + get_user_wallet_settings, update_castle_settings, + update_user_wallet_settings, ) -from .models import CastleSettings +from .models import CastleSettings, UserWalletSettings async def get_settings(user_id: str) -> CastleSettings: @@ -21,3 +24,22 @@ async def update_settings(user_id: str, data: CastleSettings) -> CastleSettings: settings = await update_castle_settings(user_id, data) return settings + + +async def get_user_wallet(user_id: str) -> UserWalletSettings: + settings = await get_user_wallet_settings(user_id) + if not settings: + settings = await create_user_wallet_settings(user_id, UserWalletSettings()) + return settings + + +async def update_user_wallet( + user_id: str, data: UserWalletSettings +) -> UserWalletSettings: + settings = await get_user_wallet_settings(user_id) + if not settings: + settings = await create_user_wallet_settings(user_id, data) + else: + settings = await update_user_wallet_settings(user_id, data) + + return settings diff --git a/static/js/index.js b/static/js/index.js index 407907a..a5eb17e 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -12,9 +12,11 @@ window.app = Vue.createApp({ accounts: [], currencies: [], settings: null, + userWalletSettings: null, isAdmin: false, isSuperUser: false, castleWalletConfigured: false, + userWalletConfigured: false, expenseDialog: { show: false, description: '', @@ -34,6 +36,11 @@ window.app = Vue.createApp({ show: false, castleWalletId: '', loading: false + }, + userWalletDialog: { + show: false, + userWalletId: '', + loading: false } } }, @@ -123,10 +130,27 @@ window.app = Vue.createApp({ this.castleWalletConfigured = false } }, + async loadUserWallet() { + try { + const response = await LNbits.api.request( + 'GET', + '/castle/api/v1/user/wallet', + this.g.user.wallets[0].inkey + ) + this.userWalletSettings = response.data + this.userWalletConfigured = !!(this.userWalletSettings && this.userWalletSettings.user_wallet_id) + } catch (error) { + this.userWalletConfigured = false + } + }, showSettingsDialog() { this.settingsDialog.castleWalletId = this.settings?.castle_wallet_id || '' this.settingsDialog.show = true }, + showUserWalletDialog() { + this.userWalletDialog.userWalletId = this.userWalletSettings?.user_wallet_id || '' + this.userWalletDialog.show = true + }, async submitSettings() { if (!this.settingsDialog.castleWalletId) { this.$q.notify({ @@ -158,6 +182,37 @@ window.app = Vue.createApp({ this.settingsDialog.loading = false } }, + async submitUserWallet() { + if (!this.userWalletDialog.userWalletId) { + this.$q.notify({ + type: 'warning', + message: 'Wallet ID is required' + }) + return + } + + this.userWalletDialog.loading = true + try { + await LNbits.api.request( + 'PUT', + '/castle/api/v1/user/wallet', + this.g.user.wallets[0].inkey, + { + user_wallet_id: this.userWalletDialog.userWalletId + } + ) + this.$q.notify({ + type: 'positive', + message: 'Wallet configured successfully' + }) + this.userWalletDialog.show = false + await this.loadUserWallet() + } catch (error) { + LNbits.utils.notifyApiError(error) + } finally { + this.userWalletDialog.loading = false + } + }, async submitExpense() { this.expenseDialog.loading = true try { @@ -252,5 +307,6 @@ window.app = Vue.createApp({ await this.loadAccounts() await this.loadCurrencies() await this.loadSettings() + await this.loadUserWallet() } }) diff --git a/templates/castle/index.html b/templates/castle/index.html index 07c0340..a39bdae 100644 --- a/templates/castle/index.html +++ b/templates/castle/index.html @@ -16,9 +16,12 @@
🏰 Castle Accounting

Track expenses, receivables, and balances for the collective

-
- - Settings (Super User Only) +
+ + Configure Your Wallet + + + Castle Settings (Super User Only)
@@ -47,6 +50,18 @@ + + +
+ Wallet Setup Required: You must configure your wallet before using this extension. +
+ +
+ @@ -91,12 +106,15 @@ Add Expense Castle wallet must be configured first + + You must configure your wallet first + View Transactions @@ -317,4 +335,32 @@ + + + + +
Configure Your Wallet
+ + + +
+ This is the wallet you'll use for Castle transactions. You can find your wallet ID in the LNbits wallet settings. +
+ +
+ + Save Wallet + + Cancel +
+
+
+
+ {% endblock %} diff --git a/views_api.py b/views_api.py index d2bc6f4..42ed6b7 100644 --- a/views_api.py +++ b/views_api.py @@ -36,8 +36,9 @@ from .models import ( ReceivableEntry, RevenueEntry, UserBalance, + UserWalletSettings, ) -from .services import get_settings, update_settings +from .services import get_settings, get_user_wallet, update_settings, update_user_wallet castle_api_router = APIRouter() @@ -56,6 +57,17 @@ async def check_castle_wallet_configured() -> str: return settings.castle_wallet_id +async def check_user_wallet_configured(user_id: str) -> str: + """Ensure user has configured their wallet, return wallet_id""" + settings = await get_user_wallet(user_id) + if not settings or not settings.user_wallet_id: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="You must configure your wallet in settings before using this feature.", + ) + return settings.user_wallet_id + + # ===== UTILITY ENDPOINTS ===== @@ -170,6 +182,11 @@ async def api_create_expense_entry( If currency is provided, amount is converted from fiat to satoshis. """ + # Check that castle wallet is configured + await check_castle_wallet_configured() + + # Check that user has configured their wallet + await check_user_wallet_configured(wallet.wallet.user) # Handle currency conversion amount_sats = int(data.amount) metadata = {} @@ -502,3 +519,32 @@ async def api_update_settings( ) user_id = "admin" return await update_settings(user_id, data) + + +# ===== USER WALLET ENDPOINTS ===== + + +@castle_api_router.get("/api/v1/user/wallet") +async def api_get_user_wallet( + user: User = Depends(check_user_exists), +) -> UserWalletSettings: + """Get current user's wallet settings""" + settings = await get_user_wallet(user.id) + # Return empty settings if not configured (so UI can show setup screen) + if not settings: + return UserWalletSettings() + return settings + + +@castle_api_router.put("/api/v1/user/wallet") +async def api_update_user_wallet( + data: UserWalletSettings, + user: User = Depends(check_user_exists), +) -> UserWalletSettings: + """Update current user's wallet settings""" + if not data.user_wallet_id: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="User wallet ID is required", + ) + return await update_user_wallet(user.id, data)