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:
parent
ceabf96f79
commit
29983cedb7
7 changed files with 199 additions and 2 deletions
32
crud.py
32
crud.py
|
|
@ -8,12 +8,14 @@ from lnbits.helpers import urlsafe_short_hash
|
||||||
from .models import (
|
from .models import (
|
||||||
Account,
|
Account,
|
||||||
AccountType,
|
AccountType,
|
||||||
|
CastleSettings,
|
||||||
CreateAccount,
|
CreateAccount,
|
||||||
CreateEntryLine,
|
CreateEntryLine,
|
||||||
CreateJournalEntry,
|
CreateJournalEntry,
|
||||||
EntryLine,
|
EntryLine,
|
||||||
JournalEntry,
|
JournalEntry,
|
||||||
UserBalance,
|
UserBalance,
|
||||||
|
UserCastleSettings,
|
||||||
)
|
)
|
||||||
|
|
||||||
db = Database("ext_castle")
|
db = Database("ext_castle")
|
||||||
|
|
@ -345,3 +347,33 @@ async def get_account_transactions(
|
||||||
transactions.append((entry, line))
|
transactions.append((entry, line))
|
||||||
|
|
||||||
return transactions
|
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
|
||||||
|
|
|
||||||
|
|
@ -125,3 +125,18 @@ async def m002_add_metadata_column(db):
|
||||||
ALTER TABLE entry_lines ADD COLUMN metadata TEXT DEFAULT '{}';
|
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}
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
|
||||||
17
models.py
17
models.py
|
|
@ -102,3 +102,20 @@ class RevenueEntry(BaseModel):
|
||||||
payment_method_account: str # e.g., "Cash", "Bank", "Lightning"
|
payment_method_account: str # e.g., "Cash", "Bank", "Lightning"
|
||||||
reference: Optional[str] = None
|
reference: Optional[str] = None
|
||||||
currency: Optional[str] = None # If None, amount is in satoshis. Otherwise, fiat currency code
|
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
23
services.py
Normal 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
|
||||||
|
|
@ -11,6 +11,8 @@ window.app = Vue.createApp({
|
||||||
transactions: [],
|
transactions: [],
|
||||||
accounts: [],
|
accounts: [],
|
||||||
currencies: [],
|
currencies: [],
|
||||||
|
settings: null,
|
||||||
|
isAdmin: false,
|
||||||
expenseDialog: {
|
expenseDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
description: '',
|
description: '',
|
||||||
|
|
@ -25,6 +27,11 @@ window.app = Vue.createApp({
|
||||||
show: false,
|
show: false,
|
||||||
amount: null,
|
amount: null,
|
||||||
loading: false
|
loading: false
|
||||||
|
},
|
||||||
|
settingsDialog: {
|
||||||
|
show: false,
|
||||||
|
castleWalletId: '',
|
||||||
|
loading: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -95,6 +102,47 @@ window.app = Vue.createApp({
|
||||||
LNbits.utils.notifyApiError(error)
|
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() {
|
async submitExpense() {
|
||||||
this.expenseDialog.loading = true
|
this.expenseDialog.loading = true
|
||||||
try {
|
try {
|
||||||
|
|
@ -188,5 +236,6 @@ window.app = Vue.createApp({
|
||||||
await this.loadTransactions()
|
await this.loadTransactions()
|
||||||
await this.loadAccounts()
|
await this.loadAccounts()
|
||||||
await this.loadCurrencies()
|
await this.loadCurrencies()
|
||||||
|
await this.loadSettings()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,17 @@
|
||||||
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<h5 class="q-my-none">🏰 Castle Accounting</h5>
|
<div class="row items-center no-wrap">
|
||||||
<p>Track expenses, receivables, and balances for the collective</p>
|
<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-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
|
|
@ -233,4 +242,32 @@
|
||||||
</q-card>
|
</q-card>
|
||||||
</q-dialog>
|
</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 %}
|
{% endblock %}
|
||||||
|
|
|
||||||
24
views_api.py
24
views_api.py
|
|
@ -22,6 +22,7 @@ from .crud import (
|
||||||
from .models import (
|
from .models import (
|
||||||
Account,
|
Account,
|
||||||
AccountType,
|
AccountType,
|
||||||
|
CastleSettings,
|
||||||
CreateAccount,
|
CreateAccount,
|
||||||
CreateEntryLine,
|
CreateEntryLine,
|
||||||
CreateJournalEntry,
|
CreateJournalEntry,
|
||||||
|
|
@ -31,6 +32,7 @@ from .models import (
|
||||||
RevenueEntry,
|
RevenueEntry,
|
||||||
UserBalance,
|
UserBalance,
|
||||||
)
|
)
|
||||||
|
from .services import get_settings, update_settings
|
||||||
|
|
||||||
castle_api_router = APIRouter()
|
castle_api_router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -450,3 +452,25 @@ async def api_pay_user(
|
||||||
"new_balance": balance.balance,
|
"new_balance": balance.balance,
|
||||||
"message": "Payment recorded successfully",
|
"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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue