Adds per-user wallet configuration
Allows users to configure their own wallet ID, enabling the system to track expenses and receivables on a per-user basis. Introduces new database table, models, API endpoints, and UI elements to manage user-specific wallet settings.
This commit is contained in:
parent
31344607c6
commit
bb1dbcccd8
7 changed files with 236 additions and 6 deletions
32
crud.py
32
crud.py
|
|
@ -14,8 +14,10 @@ from .models import (
|
||||||
CreateJournalEntry,
|
CreateJournalEntry,
|
||||||
EntryLine,
|
EntryLine,
|
||||||
JournalEntry,
|
JournalEntry,
|
||||||
|
StoredUserWalletSettings,
|
||||||
UserBalance,
|
UserBalance,
|
||||||
UserCastleSettings,
|
UserCastleSettings,
|
||||||
|
UserWalletSettings,
|
||||||
)
|
)
|
||||||
|
|
||||||
db = Database("ext_castle")
|
db = Database("ext_castle")
|
||||||
|
|
@ -377,3 +379,33 @@ async def update_castle_settings(
|
||||||
settings = UserCastleSettings(**data.dict(), id=user_id)
|
settings = UserCastleSettings(**data.dict(), id=user_id)
|
||||||
await db.update("extension_settings", settings)
|
await db.update("extension_settings", settings)
|
||||||
return 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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
|
||||||
13
models.py
13
models.py
|
|
@ -119,3 +119,16 @@ class UserCastleSettings(CastleSettings):
|
||||||
"""User-specific settings (stored with user_id)"""
|
"""User-specific settings (stored with user_id)"""
|
||||||
|
|
||||||
id: str
|
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
|
||||||
|
|
|
||||||
24
services.py
24
services.py
|
|
@ -1,9 +1,12 @@
|
||||||
from .crud import (
|
from .crud import (
|
||||||
create_castle_settings,
|
create_castle_settings,
|
||||||
|
create_user_wallet_settings,
|
||||||
get_castle_settings,
|
get_castle_settings,
|
||||||
|
get_user_wallet_settings,
|
||||||
update_castle_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:
|
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)
|
settings = await update_castle_settings(user_id, data)
|
||||||
|
|
||||||
return settings
|
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
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,11 @@ window.app = Vue.createApp({
|
||||||
accounts: [],
|
accounts: [],
|
||||||
currencies: [],
|
currencies: [],
|
||||||
settings: null,
|
settings: null,
|
||||||
|
userWalletSettings: null,
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isSuperUser: false,
|
isSuperUser: false,
|
||||||
castleWalletConfigured: false,
|
castleWalletConfigured: false,
|
||||||
|
userWalletConfigured: false,
|
||||||
expenseDialog: {
|
expenseDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
description: '',
|
description: '',
|
||||||
|
|
@ -34,6 +36,11 @@ window.app = Vue.createApp({
|
||||||
show: false,
|
show: false,
|
||||||
castleWalletId: '',
|
castleWalletId: '',
|
||||||
loading: false
|
loading: false
|
||||||
|
},
|
||||||
|
userWalletDialog: {
|
||||||
|
show: false,
|
||||||
|
userWalletId: '',
|
||||||
|
loading: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -123,10 +130,27 @@ window.app = Vue.createApp({
|
||||||
this.castleWalletConfigured = false
|
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() {
|
showSettingsDialog() {
|
||||||
this.settingsDialog.castleWalletId = this.settings?.castle_wallet_id || ''
|
this.settingsDialog.castleWalletId = this.settings?.castle_wallet_id || ''
|
||||||
this.settingsDialog.show = true
|
this.settingsDialog.show = true
|
||||||
},
|
},
|
||||||
|
showUserWalletDialog() {
|
||||||
|
this.userWalletDialog.userWalletId = this.userWalletSettings?.user_wallet_id || ''
|
||||||
|
this.userWalletDialog.show = true
|
||||||
|
},
|
||||||
async submitSettings() {
|
async submitSettings() {
|
||||||
if (!this.settingsDialog.castleWalletId) {
|
if (!this.settingsDialog.castleWalletId) {
|
||||||
this.$q.notify({
|
this.$q.notify({
|
||||||
|
|
@ -158,6 +182,37 @@ window.app = Vue.createApp({
|
||||||
this.settingsDialog.loading = false
|
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() {
|
async submitExpense() {
|
||||||
this.expenseDialog.loading = true
|
this.expenseDialog.loading = true
|
||||||
try {
|
try {
|
||||||
|
|
@ -252,5 +307,6 @@ window.app = Vue.createApp({
|
||||||
await this.loadAccounts()
|
await this.loadAccounts()
|
||||||
await this.loadCurrencies()
|
await this.loadCurrencies()
|
||||||
await this.loadSettings()
|
await this.loadSettings()
|
||||||
|
await this.loadUserWallet()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,12 @@
|
||||||
<h5 class="q-my-none">🏰 Castle Accounting</h5>
|
<h5 class="q-my-none">🏰 Castle Accounting</h5>
|
||||||
<p class="q-mb-none">Track expenses, receivables, and balances for the collective</p>
|
<p class="q-mb-none">Track expenses, receivables, and balances for the collective</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto" v-if="isSuperUser">
|
<div class="col-auto">
|
||||||
<q-btn flat round icon="settings" @click="showSettingsDialog">
|
<q-btn flat round icon="account_balance_wallet" @click="showUserWalletDialog">
|
||||||
<q-tooltip>Settings (Super User Only)</q-tooltip>
|
<q-tooltip>Configure Your Wallet</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn v-if="isSuperUser" flat round icon="settings" @click="showSettingsDialog">
|
||||||
|
<q-tooltip>Castle Settings (Super User Only)</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -47,6 +50,18 @@
|
||||||
</div>
|
</div>
|
||||||
</q-banner>
|
</q-banner>
|
||||||
|
|
||||||
|
<q-banner v-if="castleWalletConfigured && !userWalletConfigured" class="bg-orange text-white" rounded>
|
||||||
|
<template v-slot:avatar>
|
||||||
|
<q-icon name="account_balance_wallet" color="white"></q-icon>
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
|
<strong>Wallet Setup Required:</strong> You must configure your wallet before using this extension.
|
||||||
|
</div>
|
||||||
|
<template v-slot:action>
|
||||||
|
<q-btn flat color="white" label="Configure Wallet" @click="showUserWalletDialog"></q-btn>
|
||||||
|
</template>
|
||||||
|
</q-banner>
|
||||||
|
|
||||||
<!-- User Balance Card -->
|
<!-- User Balance Card -->
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
|
|
@ -91,12 +106,15 @@
|
||||||
<q-btn
|
<q-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
@click="expenseDialog.show = true"
|
@click="expenseDialog.show = true"
|
||||||
:disable="!castleWalletConfigured"
|
:disable="!castleWalletConfigured || !userWalletConfigured"
|
||||||
>
|
>
|
||||||
Add Expense
|
Add Expense
|
||||||
<q-tooltip v-if="!castleWalletConfigured">
|
<q-tooltip v-if="!castleWalletConfigured">
|
||||||
Castle wallet must be configured first
|
Castle wallet must be configured first
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
|
<q-tooltip v-if="castleWalletConfigured && !userWalletConfigured">
|
||||||
|
You must configure your wallet first
|
||||||
|
</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
<q-btn color="secondary" @click="loadTransactions">
|
<q-btn color="secondary" @click="loadTransactions">
|
||||||
View Transactions
|
View Transactions
|
||||||
|
|
@ -317,4 +335,32 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</q-dialog>
|
||||||
|
|
||||||
|
<!-- User Wallet Dialog -->
|
||||||
|
<q-dialog v-model="userWalletDialog.show" position="top">
|
||||||
|
<q-card v-if="userWalletDialog.show" class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
|
<q-form @submit="submitUserWallet" class="q-gutter-md">
|
||||||
|
<div class="text-h6 q-mb-md">Configure Your Wallet</div>
|
||||||
|
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
v-model.trim="userWalletDialog.userWalletId"
|
||||||
|
label="Your Wallet ID *"
|
||||||
|
placeholder="Enter your wallet ID"
|
||||||
|
></q-input>
|
||||||
|
|
||||||
|
<div class="text-caption text-grey">
|
||||||
|
This is the wallet you'll use for Castle transactions. You can find your wallet ID in the LNbits wallet settings.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-mt-lg">
|
||||||
|
<q-btn unelevated color="primary" type="submit" :loading="userWalletDialog.loading">
|
||||||
|
Save Wallet
|
||||||
|
</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 %}
|
{% endblock %}
|
||||||
|
|
|
||||||
48
views_api.py
48
views_api.py
|
|
@ -36,8 +36,9 @@ from .models import (
|
||||||
ReceivableEntry,
|
ReceivableEntry,
|
||||||
RevenueEntry,
|
RevenueEntry,
|
||||||
UserBalance,
|
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()
|
castle_api_router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -56,6 +57,17 @@ async def check_castle_wallet_configured() -> str:
|
||||||
return settings.castle_wallet_id
|
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 =====
|
# ===== UTILITY ENDPOINTS =====
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -170,6 +182,11 @@ async def api_create_expense_entry(
|
||||||
|
|
||||||
If currency is provided, amount is converted from fiat to satoshis.
|
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
|
# Handle currency conversion
|
||||||
amount_sats = int(data.amount)
|
amount_sats = int(data.amount)
|
||||||
metadata = {}
|
metadata = {}
|
||||||
|
|
@ -502,3 +519,32 @@ async def api_update_settings(
|
||||||
)
|
)
|
||||||
user_id = "admin"
|
user_id = "admin"
|
||||||
return await update_settings(user_id, data)
|
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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue