+
Select the wallet that will be used for Castle operations and transactions.
-
-
-
Fava/Beancount Integration
-
-
-
-
-
-
-
Account:
- """Create a new account (super user only)"""
+ """Create a new account (admin only)"""
return await create_account(data)
@castle_api_router.get("/api/v1/accounts/{account_id}")
-async def api_get_account(
- account_id: str,
- auth: AuthContext = Depends(require_authenticated),
-) -> Account:
- """Get a specific account (requires authentication and account access)"""
+async def api_get_account(account_id: str) -> Account:
+ """Get a specific account"""
account = await get_account(account_id)
if not account:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Account not found"
)
- # Check access permission
- await require_account_access(auth, account_id, PermissionType.READ)
return account
@castle_api_router.get("/api/v1/accounts/{account_id}/balance")
-async def api_get_account_balance(
- account_id: str,
- auth: AuthContext = Depends(require_authenticated),
-) -> dict:
- """Get account balance from Fava/Beancount (requires authentication and account access)"""
+async def api_get_account_balance(account_id: str) -> dict:
+ """Get account balance from Fava/Beancount"""
from .fava_client import get_fava_client
# Get account to retrieve its name
@@ -319,9 +303,6 @@ async def api_get_account_balance(
if not account:
raise HTTPException(status_code=404, detail="Account not found")
- # Check access permission
- await require_account_access(auth, account_id, PermissionType.READ)
-
# Query Fava for balance
fava = get_fava_client()
balance_data = await fava.get_account_balance(account.name)
@@ -335,16 +316,11 @@ async def api_get_account_balance(
@castle_api_router.get("/api/v1/accounts/{account_id}/transactions")
-async def api_get_account_transactions(
- account_id: str,
- limit: int = 100,
- auth: AuthContext = Depends(require_authenticated),
-) -> list[dict]:
+async def api_get_account_transactions(account_id: str, limit: int = 100) -> list[dict]:
"""
Get all transactions for an account from Fava/Beancount.
Returns transactions affecting this account in reverse chronological order.
- Requires authentication and account access.
"""
from .fava_client import get_fava_client
@@ -356,9 +332,6 @@ async def api_get_account_transactions(
detail=f"Account {account_id} not found"
)
- # Check access permission
- await require_account_access(auth, account_id, PermissionType.READ)
-
# Query Fava for transactions
fava = get_fava_client()
transactions = await fava.get_account_transactions(account.name, limit)
@@ -370,15 +343,11 @@ async def api_get_account_transactions(
@castle_api_router.get("/api/v1/entries")
-async def api_get_journal_entries(
- limit: int = 100,
- auth: AuthContext = Depends(require_super_user),
-) -> list[dict]:
+async def api_get_journal_entries(limit: int = 100) -> list[dict]:
"""
Get all journal entries from Fava/Beancount.
Returns all transactions in reverse chronological order with username enrichment.
- SUPER USER ONLY - exposes all transaction data.
"""
from lnbits.core.crud.users import get_user
from .fava_client import get_fava_client
@@ -752,15 +721,22 @@ async def _get_username_from_user_id(user_id: str) -> str:
@castle_api_router.get("/api/v1/entries/pending")
async def api_get_pending_entries(
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> list[dict]:
"""
- Get all pending expense entries that need approval (super user only).
+ Get all pending expense entries that need approval (admin only).
Returns transactions with flag='!' from Fava/Beancount.
"""
+ from lnbits.settings import settings as lnbits_settings
from .fava_client import get_fava_client
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can access this endpoint",
+ )
+
# Query Fava for all journal entries (includes links, tags, full metadata)
fava = get_fava_client()
all_entries = await fava.get_journal_entries()
@@ -973,7 +949,7 @@ async def api_create_journal_entry(
# Entry metadata (excluding tags and links which go at transaction level)
entry_meta = {k: v for k, v in data.meta.items() if k not in ["tags", "links"]}
entry_meta["source"] = "castle-api"
- entry_meta["created-by"] = wallet.wallet.user # Use user_id, not wallet_id
+ entry_meta["created-by"] = wallet.wallet.id
# Format as Beancount entry
fava = get_fava_client()
@@ -999,7 +975,7 @@ async def api_create_journal_entry(
id=f"fava-{timestamp}",
description=data.description,
entry_date=data.entry_date if data.entry_date else datetime.now(),
- created_by=wallet.wallet.user, # Use user_id, not wallet_id
+ created_by=wallet.wallet.id,
created_at=datetime.now(),
reference=data.reference,
flag=data.flag if data.flag else JournalEntryFlag.CLEARED,
@@ -1162,7 +1138,7 @@ async def api_create_expense_entry(
id=entry_id, # Use the generated castle entry ID
description=data.description + description_suffix,
entry_date=data.entry_date if data.entry_date else datetime.now(),
- created_by=wallet.wallet.user, # Use user_id, not wallet_id
+ created_by=wallet.wallet.id,
created_at=datetime.now(),
reference=castle_reference,
flag=JournalEntryFlag.PENDING,
@@ -1290,7 +1266,7 @@ async def api_create_receivable_entry(
id=entry_id, # Use the generated castle entry ID
description=data.description + description_suffix,
entry_date=datetime.now(),
- created_by=wallet.wallet.user, # Use user_id, not wallet_id
+ created_by=wallet.wallet.id,
created_at=datetime.now(),
reference=castle_reference, # Use castle reference with unique ID
flag=JournalEntryFlag.PENDING,
@@ -1404,7 +1380,7 @@ async def api_create_revenue_entry(
id=entry_id,
description=data.description,
entry_date=datetime.now(),
- created_by=wallet.wallet.user, # Use user_id, not wallet_id
+ created_by=wallet.wallet.id,
created_at=datetime.now(),
reference=castle_reference,
flag=JournalEntryFlag.CLEARED,
@@ -1468,18 +1444,8 @@ async def api_get_my_balance(
@castle_api_router.get("/api/v1/balance/{user_id}")
-async def api_get_user_balance(
- user_id: str,
- auth: AuthContext = Depends(require_authenticated),
-) -> UserBalance:
- """
- Get a specific user's balance with the Castle (from Fava/Beancount).
-
- Users can only access their own balance. Super users can access any user's balance.
- """
- # Check access: must be own data or super user
- await require_user_data_access(auth, user_id)
-
+async def api_get_user_balance(user_id: str) -> UserBalance:
+ """Get a specific user's balance with the Castle (from Fava/Beancount)"""
from .fava_client import get_fava_client
fava = get_fava_client()
@@ -1495,9 +1461,9 @@ async def api_get_user_balance(
@castle_api_router.get("/api/v1/balances/all")
async def api_get_all_balances(
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> list[dict]:
- """Get all user balances (super user only) from Fava/Beancount"""
+ """Get all user balances (admin/super user only) from Fava/Beancount"""
from .fava_client import get_fava_client
fava = get_fava_client()
@@ -1836,7 +1802,7 @@ async def api_pay_user(
@castle_api_router.post("/api/v1/receivables/settle")
async def api_settle_receivable(
data: SettleReceivable,
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> dict:
"""
Manually settle a receivable (record when user pays castle in person).
@@ -1846,8 +1812,15 @@ async def api_settle_receivable(
- Bank transfers
- Other manual settlements
- Super user only.
+ Admin only.
"""
+ from lnbits.settings import settings as lnbits_settings
+
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can settle receivables",
+ )
# Validate payment method
valid_methods = ["cash", "bank_transfer", "check", "lightning", "btc_onchain", "other"]
@@ -1984,7 +1957,7 @@ async def api_settle_receivable(
@castle_api_router.post("/api/v1/payables/pay")
async def api_pay_user(
data: PayUser,
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> dict:
"""
Pay a user (castle pays user for expense/liability).
@@ -1993,8 +1966,15 @@ async def api_pay_user(
- Lightning payments: already executed, just record the payment
- Cash/Bank/Check: record manual payment that was made
- Super user only.
+ Admin only.
"""
+ from lnbits.settings import settings as lnbits_settings
+
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can pay users",
+ )
# Validate payment method
valid_methods = ["cash", "bank_transfer", "check", "lightning", "btc_onchain", "other"]
@@ -2113,7 +2093,7 @@ async def api_pay_user(
if "meta" not in entry:
entry["meta"] = {}
entry["meta"]["payment-method"] = data.payment_method
- entry["meta"]["paid-by"] = auth.user_id
+ entry["meta"]["paid-by"] = wallet.wallet.user
if data.txid:
entry["meta"]["txid"] = data.txid
@@ -2172,26 +2152,19 @@ async def api_update_settings(
@castle_api_router.get("/api/v1/user-wallet/{user_id}")
async def api_get_user_wallet(
user_id: str,
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> dict:
- """Get user's wallet settings (super user only)
+ """Get user's wallet settings (admin only)"""
+ from lnbits.settings import settings as lnbits_settings
- Supports both full UUIDs and truncated 8-char IDs (from Beancount accounts).
- """
- from .crud import get_user_wallet_settings_by_prefix
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can access user wallet info",
+ )
- # First try exact match
user_wallet = await get_user_wallet(user_id)
-
- # If not found and user_id looks like a truncated ID (8 chars), try prefix match
- if not user_wallet or not user_wallet.user_wallet_id:
- if len(user_id) <= 8:
- stored_wallet = await get_user_wallet_settings_by_prefix(user_id)
- if stored_wallet and stored_wallet.user_wallet_id:
- user_wallet = stored_wallet
- user_id = stored_wallet.id # Use the full ID
-
- if not user_wallet or not user_wallet.user_wallet_id:
+ if not user_wallet:
return {"user_id": user_id, "user_wallet_id": None}
# Get invoice key for the user's wallet (needed to generate invoices)
@@ -2210,9 +2183,9 @@ async def api_get_user_wallet(
@castle_api_router.get("/api/v1/users")
async def api_get_all_users(
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> list[dict]:
- """Get all users who have configured their wallet (super user only)"""
+ """Get all users who have configured their wallet (admin only)"""
from lnbits.core.crud.users import get_user
user_settings = await get_all_user_wallet_settings()
@@ -2236,12 +2209,12 @@ async def api_get_all_users(
@castle_api_router.get("/api/v1/admin/castle-users")
async def api_get_castle_users(
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> list[dict]:
"""
Get all users who have configured their wallet in Castle.
These are users who can interact with Castle (submit expenses, receive permissions, etc.).
- Super user only.
+ Admin only.
"""
from lnbits.core.crud.users import get_user
@@ -2274,10 +2247,10 @@ async def api_expense_report(
start_date: Optional[str] = None,
end_date: Optional[str] = None,
group_by: str = "account",
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> dict:
"""
- Get expense summary report using BQL. Super user only.
+ Get expense summary report using BQL.
Args:
start_date: Filter from this date (YYYY-MM-DD), optional
@@ -2538,18 +2511,32 @@ async def api_get_manual_payment_requests(
@castle_api_router.get("/api/v1/manual-payment-requests/all")
async def api_get_all_manual_payment_requests(
status: str = None,
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> list[ManualPaymentRequest]:
- """Get all manual payment requests (super user only)"""
+ """Get all manual payment requests (Castle admin only)"""
+ from lnbits.settings import settings as lnbits_settings
+
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can access this endpoint",
+ )
return await get_all_manual_payment_requests(status)
@castle_api_router.post("/api/v1/manual-payment-requests/{request_id}/approve")
async def api_approve_manual_payment_request(
request_id: str,
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> ManualPaymentRequest:
- """Approve a manual payment request and create accounting entry (super user only)"""
+ """Approve a manual payment request and create accounting entry (Castle admin only)"""
+ from lnbits.settings import settings as lnbits_settings
+
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can access this endpoint",
+ )
# Get the request
request = await get_manual_payment_request(request_id)
@@ -2617,9 +2604,17 @@ async def api_approve_manual_payment_request(
@castle_api_router.post("/api/v1/manual-payment-requests/{request_id}/reject")
async def api_reject_manual_payment_request(
request_id: str,
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> ManualPaymentRequest:
- """Reject a manual payment request (super user only)"""
+ """Reject a manual payment request (Castle admin only)"""
+ from lnbits.settings import settings as lnbits_settings
+
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can access this endpoint",
+ )
+
# Get the request
request = await get_manual_payment_request(request_id)
if not request:
@@ -2634,7 +2629,7 @@ async def api_reject_manual_payment_request(
detail=f"Request already {request.status}",
)
- return await reject_manual_payment_request(request_id, auth.user_id)
+ return await reject_manual_payment_request(request_id, wallet.wallet.user)
# ===== EXPENSE APPROVAL ENDPOINTS =====
@@ -2643,22 +2638,29 @@ async def api_reject_manual_payment_request(
@castle_api_router.post("/api/v1/entries/{entry_id}/approve")
async def api_approve_expense_entry(
entry_id: str,
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> dict:
"""
- Approve a pending expense entry by changing flag from '!' to '*' (super user only).
+ Approve a pending expense entry by changing flag from '!' to '*' (admin only).
This updates the transaction in the Beancount file via Fava API.
"""
- import httpx
+ from lnbits.settings import settings as lnbits_settings
from .fava_client import get_fava_client
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can approve expenses",
+ )
+
fava = get_fava_client()
# 1. Get all journal entries from Fava
all_entries = await fava.get_journal_entries()
# 2. Find the entry with matching castle ID in links
+ target_entry_hash = None
target_entry = None
for entry in all_entries:
@@ -2670,86 +2672,51 @@ async def api_approve_expense_entry(
link_clean = link.lstrip('^')
# Check if this entry has our castle ID
if link_clean == f"castle-{entry_id}" or link_clean.endswith(f"-{entry_id}"):
+ target_entry_hash = entry.get("entry_hash")
target_entry = entry
break
- if target_entry:
+ if target_entry_hash:
break
- if not target_entry:
+ if not target_entry_hash:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail=f"Pending entry {entry_id} not found in Beancount ledger"
)
- # Get entry metadata for file location
- meta = target_entry.get("meta", {})
- filename = meta.get("filename")
- lineno = meta.get("lineno")
- date_str = target_entry.get("date", "")
+ # 3. Get the entry context (source text + sha256sum)
+ context = await fava.get_entry_context(target_entry_hash)
+ source = context.get("slice", "")
+ sha256sum = context.get("sha256sum", "")
- if not filename or not lineno:
+ if not source:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
- detail="Entry metadata missing filename or lineno"
+ detail="Could not retrieve entry source from Fava"
)
- # 3. Get the source file from Fava
- async with httpx.AsyncClient(timeout=fava.timeout) as client:
- response = await client.get(
- f"{fava.base_url}/source",
- params={"filename": filename}
+ # 4. Change flag from ! to *
+ # Replace the first occurrence of the date + ! pattern
+ import re
+ date_str = target_entry.get("date", "")
+ old_pattern = f"{date_str} !"
+ new_pattern = f"{date_str} *"
+
+ if old_pattern not in source:
+ raise HTTPException(
+ status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
+ detail=f"Could not find pending flag pattern '{old_pattern}' in entry source"
)
- response.raise_for_status()
- source_data = response.json()["data"]
- sha256sum = source_data["sha256sum"]
- source = source_data["source"]
- lines = source.split('\n')
+ new_source = source.replace(old_pattern, new_pattern, 1)
- # 4. Find and modify the entry at the specified line
- # Line numbers are 1-indexed, list is 0-indexed
- entry_line_idx = lineno - 1
-
- if entry_line_idx >= len(lines):
- raise HTTPException(
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
- detail=f"Line {lineno} not found in source file"
- )
-
- entry_line = lines[entry_line_idx]
-
- # Check if the line contains the pending flag pattern
- old_pattern = f"{date_str} !"
- if old_pattern not in entry_line:
- raise HTTPException(
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
- detail=f"Line {lineno} does not contain expected pattern '{old_pattern}'. Found: {entry_line}"
- )
-
- # Replace the flag
- new_pattern = f"{date_str} *"
- new_line = entry_line.replace(old_pattern, new_pattern, 1)
- lines[entry_line_idx] = new_line
-
- # 5. Write back the modified source
- new_source = '\n'.join(lines)
-
- update_response = await client.put(
- f"{fava.base_url}/source",
- json={
- "file_path": filename,
- "source": new_source,
- "sha256sum": sha256sum
- },
- headers={"Content-Type": "application/json"}
- )
- update_response.raise_for_status()
-
- logger.info(f"Entry {entry_id} approved (flag changed to *)")
+ # 5. Update the entry via Fava API
+ await fava.update_entry_source(target_entry_hash, new_source, sha256sum)
return {
"message": f"Entry {entry_id} approved successfully",
"entry_id": entry_id,
+ "entry_hash": target_entry_hash,
"date": date_str,
"description": target_entry.get("narration", "")
}
@@ -2758,23 +2725,30 @@ async def api_approve_expense_entry(
@castle_api_router.post("/api/v1/entries/{entry_id}/reject")
async def api_reject_expense_entry(
entry_id: str,
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> dict:
"""
- Reject a pending expense entry by marking it as voided (super user only).
+ Reject a pending expense entry by marking it as voided (admin only).
Adds #voided tag for audit trail while keeping the '!' flag.
Voided transactions are excluded from balances but preserved in the ledger.
"""
- import httpx
+ from lnbits.settings import settings as lnbits_settings
from .fava_client import get_fava_client
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can reject expenses",
+ )
+
fava = get_fava_client()
# 1. Get all journal entries from Fava
all_entries = await fava.get_journal_entries()
# 2. Find the entry with matching castle ID in links
+ target_entry_hash = None
target_entry = None
for entry in all_entries:
@@ -2786,77 +2760,58 @@ async def api_reject_expense_entry(
link_clean = link.lstrip('^')
# Check if this entry has our castle ID
if link_clean == f"castle-{entry_id}" or link_clean.endswith(f"-{entry_id}"):
+ target_entry_hash = entry.get("entry_hash")
target_entry = entry
break
- if target_entry:
+ if target_entry_hash:
break
- if not target_entry:
+ if not target_entry_hash:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail=f"Pending entry {entry_id} not found in Beancount ledger"
)
- # Get entry metadata for file location
- meta = target_entry.get("meta", {})
- filename = meta.get("filename")
- lineno = meta.get("lineno")
- date_str = target_entry.get("date", "")
+ # 3. Get the entry context (source text + sha256sum)
+ context = await fava.get_entry_context(target_entry_hash)
+ source = context.get("slice", "")
+ sha256sum = context.get("sha256sum", "")
- if not filename or not lineno:
+ if not source:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
- detail="Entry metadata missing filename or lineno"
+ detail="Could not retrieve entry source from Fava"
)
- # 3. Get the source file from Fava
- async with httpx.AsyncClient(timeout=fava.timeout) as client:
- response = await client.get(
- f"{fava.base_url}/source",
- params={"filename": filename}
- )
- response.raise_for_status()
- source_data = response.json()["data"]
+ # 4. Add #voided tag (keep ! flag as per convention)
+ date_str = target_entry.get("date", "")
- sha256sum = source_data["sha256sum"]
- source = source_data["source"]
+ # Add #voided tag if not already present
+ if "#voided" not in source:
+ # Find the transaction line and add #voided to the tags
+ # Pattern: date ! "narration" #existing-tags
lines = source.split('\n')
+ for i, line in enumerate(lines):
+ if date_str in line and '"' in line and '!' in line:
+ # Add #voided tag to the transaction line
+ if '#' in line:
+ # Already has tags, append voided
+ lines[i] = line.rstrip() + ' #voided'
+ else:
+ # No tags yet, add after narration
+ lines[i] = line.rstrip() + ' #voided'
+ break
+ new_source = '\n'.join(lines)
+ else:
+ new_source = source
- # 4. Find and modify the entry at the specified line - add #voided tag
- entry_line_idx = lineno - 1
-
- if entry_line_idx >= len(lines):
- raise HTTPException(
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
- detail=f"Line {lineno} not found in source file"
- )
-
- entry_line = lines[entry_line_idx]
-
- # Add #voided tag if not already present
- if "#voided" not in entry_line:
- # Add #voided tag to the transaction line
- new_line = entry_line.rstrip() + ' #voided'
- lines[entry_line_idx] = new_line
-
- # 5. Write back the modified source
- new_source = '\n'.join(lines)
-
- update_response = await client.put(
- f"{fava.base_url}/source",
- json={
- "file_path": filename,
- "source": new_source,
- "sha256sum": sha256sum
- },
- headers={"Content-Type": "application/json"}
- )
- update_response.raise_for_status()
- logger.info(f"Entry {entry_id} rejected (added #voided tag)")
+ # 5. Update the entry via Fava API
+ await fava.update_entry_source(target_entry_hash, new_source, sha256sum)
return {
"message": f"Entry {entry_id} rejected (marked as voided)",
"entry_id": entry_id,
+ "entry_hash": target_entry_hash,
"date": date_str,
"description": target_entry.get("narration", "")
}
@@ -2868,10 +2823,10 @@ async def api_reject_expense_entry(
@castle_api_router.post("/api/v1/assertions")
async def api_create_balance_assertion(
data: CreateBalanceAssertion,
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> BalanceAssertion:
"""
- Create a balance assertion for reconciliation (super user only).
+ Create a balance assertion for reconciliation (admin only).
Uses hybrid approach:
1. Writes balance assertion to Beancount (via Fava) - source of truth
@@ -2880,9 +2835,16 @@ async def api_create_balance_assertion(
The assertion will be checked immediately upon creation.
"""
+ from lnbits.settings import settings as lnbits_settings
from .fava_client import get_fava_client
from .beancount_format import format_balance
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can create balance assertions",
+ )
+
# Verify account exists
account = await get_account(data.account_id)
if not account:
@@ -2914,7 +2876,7 @@ async def api_create_balance_assertion(
)
# Store metadata in Castle DB for UI convenience
- assertion = await create_balance_assertion(data, auth.user_id)
+ assertion = await create_balance_assertion(data, wallet.wallet.user)
# Check it immediately (queries Fava for actual balance)
try:
@@ -2949,9 +2911,16 @@ async def api_get_balance_assertions(
account_id: str = None,
status: str = None,
limit: int = 100,
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> list[BalanceAssertion]:
- """Get balance assertions with optional filters (super user only)"""
+ """Get balance assertions with optional filters (admin only)"""
+ from lnbits.settings import settings as lnbits_settings
+
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can view balance assertions",
+ )
# Parse status enum if provided
status_enum = None
@@ -2974,9 +2943,17 @@ async def api_get_balance_assertions(
@castle_api_router.get("/api/v1/assertions/{assertion_id}")
async def api_get_balance_assertion(
assertion_id: str,
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> BalanceAssertion:
- """Get a specific balance assertion (super user only)"""
+ """Get a specific balance assertion (admin only)"""
+ from lnbits.settings import settings as lnbits_settings
+
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can view balance assertions",
+ )
+
assertion = await get_balance_assertion(assertion_id)
if not assertion:
raise HTTPException(
@@ -2990,9 +2967,17 @@ async def api_get_balance_assertion(
@castle_api_router.post("/api/v1/assertions/{assertion_id}/check")
async def api_check_balance_assertion(
assertion_id: str,
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> BalanceAssertion:
- """Re-check a balance assertion (super user only)"""
+ """Re-check a balance assertion (admin only)"""
+ from lnbits.settings import settings as lnbits_settings
+
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can check balance assertions",
+ )
+
try:
assertion = await check_balance_assertion(assertion_id)
except ValueError as e:
@@ -3007,9 +2992,17 @@ async def api_check_balance_assertion(
@castle_api_router.delete("/api/v1/assertions/{assertion_id}")
async def api_delete_balance_assertion(
assertion_id: str,
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> dict:
- """Delete a balance assertion (super user only)"""
+ """Delete a balance assertion (admin only)"""
+ from lnbits.settings import settings as lnbits_settings
+
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can delete balance assertions",
+ )
+
# Verify it exists
assertion = await get_balance_assertion(assertion_id)
if not assertion:
@@ -3028,9 +3021,16 @@ async def api_delete_balance_assertion(
@castle_api_router.get("/api/v1/reconciliation/summary")
async def api_get_reconciliation_summary(
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> dict:
- """Get reconciliation summary (super user only)"""
+ """Get reconciliation summary (admin only)"""
+ from lnbits.settings import settings as lnbits_settings
+
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can access reconciliation",
+ )
# Get all assertions
all_assertions = await get_balance_assertions(limit=1000)
@@ -3079,9 +3079,16 @@ async def api_get_reconciliation_summary(
@castle_api_router.post("/api/v1/reconciliation/check-all")
async def api_check_all_assertions(
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> dict:
- """Re-check all balance assertions (super user only)"""
+ """Re-check all balance assertions (admin only)"""
+ from lnbits.settings import settings as lnbits_settings
+
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can run reconciliation checks",
+ )
# Get all assertions
all_assertions = await get_balance_assertions(limit=1000)
@@ -3110,9 +3117,16 @@ async def api_check_all_assertions(
@castle_api_router.get("/api/v1/reconciliation/discrepancies")
async def api_get_discrepancies(
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> dict:
- """Get all discrepancies (failed assertions, flagged entries) (super user only)"""
+ """Get all discrepancies (failed assertions, flagged entries) (admin only)"""
+ from lnbits.settings import settings as lnbits_settings
+
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can view discrepancies",
+ )
# Get failed assertions
failed_assertions = await get_balance_assertions(
@@ -3140,14 +3154,21 @@ async def api_get_discrepancies(
@castle_api_router.post("/api/v1/tasks/daily-reconciliation")
async def api_run_daily_reconciliation(
- auth: AuthContext = Depends(require_super_user),
+ wallet: WalletTypeInfo = Depends(require_admin_key),
) -> dict:
"""
- Manually trigger the daily reconciliation check (super user only).
+ Manually trigger the daily reconciliation check (admin only).
This endpoint can also be called via cron job.
Returns a summary of the reconciliation check results.
"""
+ from lnbits.settings import settings as lnbits_settings
+
+ if wallet.wallet.user != lnbits_settings.super_user:
+ raise HTTPException(
+ status_code=HTTPStatus.FORBIDDEN,
+ detail="Only super user can run daily reconciliation",
+ )
from .tasks import check_all_balance_assertions